Editor/Ground/ArchTerrain.cs
using System;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// The probe comes from ArchScene - generators cannot reach the scene; a missing probe falls back to the caller's datum.
public readonly struct ArchGround {
	readonly Scene scene;
	readonly Func<Vector2, float?> probe;

	public ArchGround( Scene scene ) {
		this.scene = scene;
		probe = null;
	}

	ArchGround( Func<Vector2, float?> probe ) {
		scene = null;
		this.probe = probe;
	}

	// For tests: the harness has no terrain, so down-standing geometry could never be measured without this.
	public static ArchGround Sampled( Func<Vector2, float?> probe ) => new( probe );

	// Buried below grade: a footing ending exactly on it floats the moment the terrain moves.
	public static float Embedment( ArchKit kit ) => MathF.Max( 2f, kit?.PierEmbedment ?? 12f );

	public float Under( Vector2 point, float datum ) {
		if ( probe is not null ) {
			return probe( point ) ?? datum;
		}

		return ArchTerrain.Sample( scene, point, out var height ) ? height : datum;
	}
}

// Height stamps are requested by placement; control-map excavation follows every committed basement edit.
public static class ArchTerrain {
	const float RelookDelay = 1f;

	// Bumped whenever the heightmap is written, so anything holding a height read off it knows to ask again.
	public static int Stamped { get; private set; }
	public static int HeightStamped { get; private set; }

	static Terrain field;
	static Scene fieldScene;
	static RealTimeSince looked;

	// Found once and kept: a marching ray used to re-walk the whole scene per step; no terrain re-looks on a delay.
	static Terrain Field( Scene scene ) {
		if ( fieldScene == scene && (field.IsValid() || looked < RelookDelay) ) {
			return field.IsValid() ? field : null;
		}

		fieldScene = scene;
		field = scene?.GetAllComponents<Terrain>().FirstOrDefault();
		looked = 0f;

		return field;
	}

	// Draping reads bilinear: a node snapped to the nearest sample steps every time the mouse crosses a cell.
	public static bool Sample( Scene scene, Vector2 world, out float height ) {
		height = 0f;

		var terrain = Field( scene );

		if ( !terrain.IsValid() || terrain.Storage is null ) {
			return false;
		}

		var storage = terrain.Storage;
		var resolution = storage.Resolution;
		var map = storage.HeightMap;

		if ( resolution < 2 || map is null || map.Length < resolution * resolution ) {
			return false;
		}

		var origin = terrain.WorldPosition;
		var step = storage.TerrainSize / (resolution - 1);
		var maxHeight = MathF.Max( 1f, storage.TerrainHeight );

		var fx = Math.Clamp( (world.x - origin.x) / step, 0f, resolution - 1.001f );
		var fy = Math.Clamp( (world.y - origin.y) / step, 0f, resolution - 1.001f );

		var x = (int)fx;
		var y = (int)fy;

		var lower = MathX.Lerp( Height( map, resolution, x, y ), Height( map, resolution, x + 1, y ), fx - x );
		var upper = MathX.Lerp( Height( map, resolution, x, y + 1 ), Height( map, resolution, x + 1, y + 1 ), fx - x );

		height = origin.z + MathX.Lerp( lower, upper, fy - y ) * maxHeight;

		return true;
	}

	// Marched over the heightmap, not collision: sculpted terrain has none, and cursor and drape must agree.
	public static bool Ground( Scene scene, Ray ray, out Vector3 hit ) {
		hit = default;

		var terrain = Field( scene );

		if ( !terrain.IsValid() || terrain.Storage is null || MathF.Abs( ray.Forward.z ) < 0.0001f ) {
			return false;
		}

		var origin = terrain.WorldPosition;
		var size = terrain.Storage.TerrainSize;
		var step = MathF.Max( 16f, size / 1024f );
		var reach = size * 3f;
		var above = 0f;

		for ( var travelled = step; travelled <= reach; travelled += step ) {
			if ( !Under( scene, ray, travelled, origin, size ) ) {
				above = travelled;
				continue;
			}

			hit = Surface( scene, ray, above, travelled, origin, size );

			return true;
		}

		return false;
	}

	// Halved down until the crossing is under an inch wide, which is finer than the grid snap that reads it.
	static Vector3 Surface( Scene scene, Ray ray, float above, float below, Vector3 origin, float size ) {
		for ( var pass = 0; pass < 16 && below - above > 1f; pass++ ) {
			var middle = (above + below) * 0.5f;

			if ( Under( scene, ray, middle, origin, size ) ) {
				below = middle;
				continue;
			}

			above = middle;
		}

		return ray.Position + ray.Forward * below;
	}

	// Outside the footprint nothing is under: Sample clamps to the edge, so the horizon must not count.
	static bool Under( Scene scene, Ray ray, float travelled, Vector3 origin, float size ) {
		var at = ray.Position + ray.Forward * travelled;

		if ( at.x < origin.x || at.y < origin.y || at.x > origin.x + size || at.y > origin.y + size ) {
			return false;
		}

		return Sample( scene, new Vector2( at.x, at.y ), out var height ) && at.z < height;
	}

	static float Height( ushort[] map, int resolution, int x, int y ) {
		var index = Math.Clamp( y, 0, resolution - 1 ) * resolution + Math.Clamp( x, 0, resolution - 1 );

		return map[index] / (float)ushort.MaxValue;
	}

	// What a stamp needs UNDER the ground to work with. A heightmap sample cannot go below zero, so terrain left at
	// the bottom of its range has nowhere for a road to be carved into and the stamp reads as doing nothing.
	public const float Reserve = 64f;

	// The whole heightmap lifted and the terrain dropped by the same amount: the surface stays exactly where it is
	// in the world - nothing standing on it moves - and the room appears beneath it instead.
	public static int Headroom( Scene scene ) {
		var terrain = Field( scene );

		if ( !terrain.IsValid() || terrain.Storage is null ) {
			Log.Warning( "Architecture: no Terrain component in this scene to give headroom." );
			return 0;
		}

		var storage = terrain.Storage;
		var resolution = storage.Resolution;
		var map = storage.HeightMap;

		if ( resolution < 2 || map is null || map.Length < resolution * resolution ) {
			Log.Warning( "Architecture: terrain has no usable heightmap." );
			return 0;
		}

		var samples = resolution * resolution;
		var maxHeight = MathF.Max( 1f, storage.TerrainHeight );
		var wanted = (int)MathF.Ceiling( Reserve / maxHeight * ushort.MaxValue );
		var lowest = ushort.MaxValue;
		var highest = (ushort)0;

		for ( var index = 0; index < samples; index++ ) {
			lowest = Math.Min( lowest, map[index] );
			highest = Math.Max( highest, map[index] );
		}

		if ( lowest >= wanted ) {
			Log.Info( $"Architecture: the ground already stands {lowest / (float)ushort.MaxValue * maxHeight:0.#} units above the bottom of its range." );
			return 0;
		}

		var lift = wanted - lowest;

		// Clamping instead would flatten the peaks to buy room at the bottom, which is a map quietly ruined.
		if ( highest + lift > ushort.MaxValue ) {
			Log.Warning( $"Architecture: this terrain's height range is too full to lift - raise its Terrain Height above {maxHeight:0.#} first." );
			return 0;
		}

		for ( var index = 0; index < samples; index++ ) {
			map[index] = (ushort)(map[index] + lift);
		}

		var risen = lift / (float)ushort.MaxValue * maxHeight;

		terrain.WorldPosition = terrain.WorldPosition.WithZ( terrain.WorldPosition.z - risen );
		terrain.SyncGPUTexture();
		terrain.UpdateCollision( Terrain.SyncFlags.Height, new RectInt( 0, 0, resolution, resolution ) );

		Stamped++;
		HeightStamped++;

		Log.Info( $"Architecture: opened {risen:0.#} units under the ground - the surface has not moved, and a stamp can now cut into it." );

		return samples;
	}

	public static int Flatten( Scene scene, ArchPlan plan, ArchKit kit ) {
		return Flatten( scene, plan, kit, plan.Buildings, true );
	}

	public static int Flatten( Scene scene, ArchPlan plan, ArchKit kit, IReadOnlyList<ArchBuilding> buildings ) {
		return Flatten( scene, plan, kit, buildings, false );
	}

	static int Flatten( Scene scene, ArchPlan plan, ArchKit kit, IReadOnlyList<ArchBuilding> buildings, bool includeContributions ) {
		var terrain = Field( scene );

		if ( !terrain.IsValid() ) {
			if ( includeContributions ) {
				Log.Warning( "Architecture: no Terrain component in this scene to flatten." );
			}

			return 0;
		}

		if ( terrain.Storage is null ) {
			Log.Warning( "Architecture: the Terrain component has no storage to flatten." );
			return 0;
		}

		var storage = terrain.Storage;
		var resolution = storage.Resolution;
		var map = storage.HeightMap;

		if ( resolution < 2 || map is null || map.Length < resolution * resolution ) {
			Log.Warning( "Architecture: terrain has no usable heightmap." );
			return 0;
		}

		var size = storage.TerrainSize;
		var maxHeight = MathF.Max( 1f, storage.TerrainHeight );
		var origin = terrain.WorldPosition;
		var step = size / (resolution - 1);
		var padding = MathF.Max( 0f, kit.TerrainPadding );
		var stamped = 0;

		if ( includeContributions ) {
			// Drives first, roads second: a drive runs out over the verge, and that stretch of ground belongs to the road.
			var terrainRequest = new ArchTerrainRequest( terrain, plan, kit, map, resolution, origin, step, maxHeight );
			var contributions = ArchTerrainContributions.Load();

			stamped += contributions.Raise( terrainRequest );
			stamped += contributions.Carve( terrainRequest );
			// Cuttings after the roads: a cutting only ever lowers what the road carve left.
			stamped += contributions.Cut( terrainRequest );
		}

		foreach ( var building in buildings ) {
			if ( !Footprint( building, out var min, out var max, out var pad ) ) {
				continue;
			}

			var target = (pad - origin.z) / maxHeight * ushort.MaxValue;
			var clamped = (ushort)Math.Clamp( target, 0f, ushort.MaxValue );

			var lo = Cell( min - new Vector2( padding, padding ), origin, step, resolution );
			var hi = Cell( max + new Vector2( padding, padding ), origin, step, resolution );

			for ( var y = lo.y; y <= hi.y; y++ ) {
				for ( var x = lo.x; x <= hi.x; x++ ) {
					var world = new Vector2( origin.x + x * step, origin.y + y * step );
					var weight = Weight( world, min, max, padding );

					if ( weight <= 0f ) {
						continue;
					}

					var index = y * resolution + x;
					var current = map[index];

					map[index] = (ushort)Math.Clamp( current + (clamped - current) * weight, 0f, ushort.MaxValue );
					stamped++;
				}
			}
		}

		if ( stamped == 0 ) {
			return 0;
		}

		terrain.SyncGPUTexture();
		terrain.UpdateCollision( Terrain.SyncFlags.Height, new RectInt( 0, 0, resolution, resolution ) );

		Stamped++;
		HeightStamped++;

		Log.Info( includeContributions
			? $"Architecture: stamped {stamped} terrain samples under buildings, roads and drives."
			: $"Architecture: stamped {stamped} terrain samples under the placed building footprint." );

		return stamped;
	}

	public static int Excavate( Scene scene, ArchPlan plan, ArchKit kit, ArchBuilding building ) {
		var stamped = Excavate( scene, plan, kit, building, true );

		if ( stamped > 0 ) {
			RefreshControl( scene );
		}

		return stamped;
	}

	public static int ExcavateBasements( Scene scene, ArchPlan plan, ArchKit kit ) {
		return ExcavateBasements( scene, plan, kit, plan.Buildings );
	}

	public static int ExcavateBasements( Scene scene, ArchPlan plan, ArchKit kit, IReadOnlyList<ArchBuilding> buildings ) {
		if ( !Field( scene ).IsValid() ) {
			return 0;
		}

		var stamped = buildings
			.Where( building => building is not null )
			.Distinct()
			.Where( building => building.Rooms.Any( room => room.Floor < 0 && room.HasFootprint ) )
			.Sum( building => Excavate( scene, plan, kit, building, false ) );

		if ( stamped > 0 ) {
			RefreshControl( scene );
			Log.Info( $"Architecture: reconciled {stamped} terrain samples with the current basement openings." );
		}

		return stamped;
	}

	static int Excavate( Scene scene, ArchPlan plan, ArchKit kit, ArchBuilding building, bool report ) {
		var basements = building?.Rooms
			.Where( room => room.Floor < 0 && room.HasFootprint )
			.ToList();

		if ( basements is not { Count: > 0 } ) {
			if ( report ) {
				Log.Warning( "Architecture: this building has no basement floors to excavate." );
			}

			return 0;
		}

		var terrain = Field( scene );

		if ( !terrain.IsValid() || terrain.Storage is null ) {
			return 0;
		}

		var storage = terrain.Storage;
		var resolution = storage.Resolution;
		var map = storage.ControlMap;

		if ( resolution < 2 || map is null || map.Length < resolution * resolution ) {
			Log.Warning( "Architecture: terrain has no usable control map." );
			return 0;
		}

		var origin = terrain.WorldPosition;
		var step = storage.TerrainSize / (resolution - 1);
		var stamped = 0;
		var basementFootprints = basements
			.Select( ArchFloorGen.Footprint )
			.Where( footprint => footprint.Count >= 3 )
			.ToList();
		var groundRegion = ArchFootprint.Union( building.Rooms
			.Where( room => room.Floor == 0 && room.HasFootprint && ArchLayerGate.On( room ) )
			.Select( ArchFloorGen.Footprint )
			.Where( footprint => footprint.Count >= 3 ) );

		foreach ( var footprint in ArchFootprint.Union( basementFootprints ) ) {
			stamped += Fill( map, resolution, origin, step, footprint );
		}

		foreach ( var well in ArchCut.Stored( building, 0 ) ) {
			stamped += Hole( map, resolution, origin, step, well.Outline(), groundRegion );
		}

		foreach ( var room in building.Rooms.Where( room => room.Floor == 0 && room.HasFloor && ArchLayerGate.On( room ) ) ) {
			var top = room.BaseHeight;
			var (wells, _) = ArchFloorGen.Bites( plan, 0, kit, top - kit.FloorThickness, top,
				building.Id, ArchCutAffects.Floors, room.Id );

			foreach ( var well in wells ) {
				stamped += Hole( map, resolution, origin, step, well.Outline(), groundRegion );
			}
		}

		foreach ( var room in basements ) {
			foreach ( var approach in room.Approaches ) {
				if ( approach.IsSpline ) {
					foreach ( var run in ArchApproachGen.SplineRuns( approach, room, building, kit, plan ) ) {
						stamped += Hole( map, resolution, origin, step, ApproachHole( run, kit ) );
					}
				} else if ( ArchApproachGen.ResolvedTerrain( approach, room, building, kit, plan, out var run ) ) {
					stamped += Hole( map, resolution, origin, step, ApproachHole( run, kit ) );
				}
			}

			foreach ( var wall in room.Walls ) {
				foreach ( var opening in wall.Openings ) {
					if ( ArchBasementWell.Resolve( room, wall, opening, building, kit ) is { } well ) {
						stamped += Hole( map, resolution, origin, step, well.Outline );
					}
				}
			}
		}

		if ( stamped == 0 ) {
			if ( report ) {
				Log.Info( "Architecture: the terrain already matches this basement's Ground openings and exterior access." );
			}

			return 0;
		}

		if ( report ) {
			Log.Info( $"Architecture: updated {stamped} terrain samples around {building.Name}'s Ground openings and exterior access." );
		}

		return stamped;
	}

	static void RefreshControl( Scene scene ) {
		var terrain = Field( scene );

		if ( !terrain.IsValid() || terrain.Storage is null ) {
			return;
		}

		var resolution = terrain.Storage.Resolution;

		terrain.SyncGPUTexture();
		terrain.UpdateCollision( Terrain.SyncFlags.Control, new RectInt( 0, 0, resolution, resolution ) );

		Stamped++;
	}

	static int Fill( uint[] map, int resolution, Vector3 origin, float step, IReadOnlyList<Vector2> footprint ) {
		return PaintHole( map, resolution, origin, step, footprint, false );
	}

	static int Hole(
		uint[] map,
		int resolution,
		Vector3 origin,
		float step,
		IReadOnlyList<Vector2> footprint,
		IReadOnlyList<List<Vector2>> inside = null ) {
		return PaintHole( map, resolution, origin, step, footprint, true, inside );
	}

	static int PaintHole(
		uint[] map,
		int resolution,
		Vector3 origin,
		float step,
		IReadOnlyList<Vector2> footprint,
		bool hole,
		IReadOnlyList<List<Vector2>> inside = null ) {
		if ( footprint.Count < 3 ) {
			return 0;
		}

		ArchFootprint.Bounds( footprint, out var min, out var max );

		var halfCell = new Vector2( step * 0.5f, step * 0.5f );
		var lo = Cell( min - halfCell, origin, step, resolution );
		var hi = Cell( max + halfCell, origin, step, resolution );
		var stamped = 0;

		for ( var y = lo.y; y <= hi.y; y++ ) {
			for ( var x = lo.x; x <= hi.x; x++ ) {
				var world = new Vector2( origin.x + x * step, origin.y + y * step );

				var cell = ArchFootprint.Rect( world - halfCell, world + halfCell );

				if ( !ArchFootprint.Contains( footprint, world ) && !ArchFootprint.Overlaps( footprint, cell ) ) {
					continue;
				}

				if ( hole && inside is { Count: > 0 } && !ArchFootprint.Encloses( inside, world ) ) {
					continue;
				}

				var index = y * resolution + x;
				var material = new CompactTerrainMaterial( map[index] );

				if ( material.IsHole == hole ) {
					continue;
				}

				material.IsHole = hole;
				map[index] = material.Packed;
				stamped++;
			}
		}

		return stamped;
	}

	static List<Vector2> ApproachHole( ArchApproachGen.ArchRampRun run, ArchKit kit ) {
		var span = run.Foot - run.Wall;
		var direction = span.IsNearZeroLength ? new Vector2( 1f, 0f ) : span.Normal;
		var across = new Vector2( -direction.y, direction.x );
		var half = run.HalfWidth + MathF.Max( ArchLap.Bite( kit ), kit.WallThickness * 0.5f );
		var reach = direction * MathF.Max( ArchLap.Bite( kit ), kit.WallThickness * 0.5f );

		return new List<Vector2>
		{
			run.Wall - reach + across * half,
			run.Foot + reach + across * half,
			run.Foot + reach - across * half,
			run.Wall - reach - across * half
		};
	}

	static bool Footprint( ArchBuilding building, out Vector2 min, out Vector2 max, out float pad ) {
		min = new Vector2( float.MaxValue, float.MaxValue );
		max = new Vector2( float.MinValue, float.MinValue );
		pad = 0f;

		var ground = building.Rooms.Where( room => room.Floor == 0 ).ToList();

		if ( ground.Count == 0 && building.Rooms.Count > 0 ) {
			var nearest = building.Rooms.Select( room => room.Floor ).OrderBy( floor => Math.Abs( floor ) ).First();

			ground = building.Rooms.Where( room => room.Floor == nearest ).ToList();
		}

		if ( ground.Count == 0 ) {
			return false;
		}

		var found = false;

		foreach ( var room in ground ) {
			foreach ( var point in ArchFloorGen.Footprint( room ) ) {
				min = new Vector2( MathF.Min( min.x, point.x ), MathF.Min( min.y, point.y ) );
				max = new Vector2( MathF.Max( max.x, point.x ), MathF.Max( max.y, point.y ) );
				found = true;
			}
		}

		pad = ground.Min( room => room.BaseHeight );

		return found;
	}

	static (int x, int y) Cell( Vector2 world, Vector3 origin, float step, int resolution ) {
		var x = (int)MathF.Round( (world.x - origin.x) / step );
		var y = (int)MathF.Round( (world.y - origin.y) / step );

		return (Math.Clamp( x, 0, resolution - 1 ), Math.Clamp( y, 0, resolution - 1 ));
	}

	// Full strength inside the footprint, easing to nothing across the padding skirt.
	static float Weight( Vector2 point, Vector2 min, Vector2 max, float padding ) {
		var outside = new Vector2(
			MathF.Max( MathF.Max( min.x - point.x, point.x - max.x ), 0f ),
			MathF.Max( MathF.Max( min.y - point.y, point.y - max.y ), 0f ) );

		var distance = outside.Length;

		if ( distance <= 0.001f ) {
			return 1f;
		}

		if ( padding <= 0.001f || distance >= padding ) {
			return 0f;
		}

		return Ease( 1f - distance / padding );
	}

	static float Ease( float t ) {
		var clamped = Math.Clamp( t, 0f, 1f );

		return clamped * clamped * (3f - 2f * clamped);
	}
}