TerrainBrushes.cs
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using Sandbox;
using Sandbox.Utility;

namespace SubZero.SubTerrain;

static class TerrainBrushes
{
	public struct Context
	{
		public ushort[] Heights;
		public ushort[] Base;
		public uint[] Control;
		public int Res;
		public int X0;
		public int Y0;
		public int X1;
		public int Y1;
		public float OriginX;
		public float OriginY;
		public float RadiusTexels;
		public float Ca;
		public float Sa;
		public float StretchX;
		public float StretchY;
		public float Ox;
		public float Oy;
		public float Oz;
		public float Freq;
		public float Irreg;
		public int DepthU;
		public int MaxDepthU;
		public int RaiseU;
		public float BowlFrac;
		public float SmoothK;
		public int DugU;
		public bool Paint;
		public int PaintIndex;
		public float PaintStrength;
		public int FlattenTargetU;
		public float FlattenStrength;
		public int FlattenMaxDeltaU;
		public float SpoilAngle;
		public bool Painted;
		public int Written;
		public int VolumeU;
	}

	public static void ApplyCrater( ref Context ctx )
	{
		ApplyBowl( ref ctx, flatFloor: false, spoil: false );
	}

	public static void ApplyDig( ref Context ctx )
	{
		ApplyBowl( ref ctx, flatFloor: true, spoil: true );
	}

	public static void ApplyFlatten( ref Context ctx )
	{
		for ( var y = ctx.Y0; y < ctx.Y1; y++ )
		{
			for ( var x = ctx.X0; x < ctx.X1; x++ )
			{
				var dx = (x + 0.5f) - ctx.OriginX;
				var dy = (y + 0.5f) - ctx.OriginY;
				var t = MathF.Sqrt( dx * dx + dy * dy ) / ctx.RadiusTexels;
				if ( t >= 1f )
					continue;

				ctx.Written++;
				var i = x + y * ctx.Res;
				var h = (int)ctx.Heights[i];
				var falloff = 1f - t;
				falloff *= falloff;
				var target = Math.Clamp( ctx.FlattenTargetU, h - ctx.FlattenMaxDeltaU, h + ctx.FlattenMaxDeltaU );
				var next = (int)MathF.Round( h + (target - h) * ctx.FlattenStrength * falloff );
				ctx.VolumeU += h - next;
				ctx.Heights[i] = (ushort)Math.Clamp( next, 0, ushort.MaxValue );

				if ( !ctx.Paint || falloff < 0.08f || ctx.Control == null )
					continue;

				Paint( ref ctx, i, falloff );
			}
		}
	}

	static void ApplyBowl( ref Context ctx, bool flatFloor, bool spoil )
	{
		for ( var y = ctx.Y0; y < ctx.Y1; y++ )
		{
			for ( var x = ctx.X0; x < ctx.X1; x++ )
			{
				var dx = (x + 0.5f) - ctx.OriginX;
				var dy = (y + 0.5f) - ctx.OriginY;
				var rx = dx * ctx.Ca + dy * ctx.Sa;
				var ry = -dx * ctx.Sa + dy * ctx.Ca;
				var dist = MathF.Sqrt( (rx / ctx.StretchX) * (rx / ctx.StretchX) + (ry / ctx.StretchY) * (ry / ctx.StretchY) );
				var theta = MathF.Atan2( ry, rx );
				var n1 = SignedFbm( 3, MathF.Cos( theta ) * ctx.Freq + ctx.Ox, MathF.Sin( theta ) * ctx.Freq + ctx.Oy );
				var n2 = SignedFbm( 2, MathF.Cos( theta ) * ctx.Freq * 2.6f + ctx.Oy, MathF.Sin( theta ) * ctx.Freq * 2.6f + ctx.Oz );
				var radiusScale = Math.Clamp( 1f + (n1 * 0.72f + n2 * 0.28f) * ctx.Irreg, 0.5f, 1.65f );
				var t = dist / (ctx.RadiusTexels * radiusScale);
				if ( t >= 1f )
					continue;

				ctx.Written++;
				var i = x + y * ctx.Res;
				var b = (int)ctx.Base[i];
				var h = (int)ctx.Heights[i];
				var bowlWeight = BowlWeight( t, ctx.BowlFrac, flatFloor );
				var slopeN = SignedFbm( 2, x * 0.09f + ctx.Ox, y * 0.09f + ctx.Oy );
				var depthMul = 1f + slopeN * ctx.Irreg * 0.12f * Math.Clamp( t / Math.Max( ctx.BowlFrac, 0.01f ), 0f, 1f );
				var oldDig = Math.Max( 0, b - h );
				var newDig = Math.Max( 0, (int)(bowlWeight * ctx.DepthU * depthMul) );
				var mergedDig = SmoothMax( oldDig, newDig, ctx.SmoothK );
				h = b - (int)MathF.Round( mergedDig );
				h = Math.Max( h, b - ctx.MaxDepthU );

				if ( spoil )
				{
					var spoilBias = MathF.Cos( theta - ctx.SpoilAngle );
					if ( t > 0.62f && spoilBias > 0.12f && oldDig < ctx.DugU )
					{
						var spoilW = (1f - t) * spoilBias;
						var rimTarget = b + (int)(spoilW * ctx.RaiseU);
						h = Math.Max( h, rimTarget );
					}
				}
				else
				{
					var rimWeight = RimWeight( t, ctx.BowlFrac );
					if ( rimWeight > 0.01f && bowlWeight < 0.25f && oldDig < ctx.DugU )
					{
						var rimN = SignedFbm( 2, MathF.Cos( theta ) * 1.7f + ctx.Oz, MathF.Sin( theta ) * 1.7f + ctx.Ox );
						var rimTarget = b + (int)(rimWeight * ctx.RaiseU * (0.55f + 0.6f * (rimN * 0.5f + 0.5f)));
						h = Math.Max( h, rimTarget );
					}
				}

				var next = (ushort)Math.Clamp( h, 0, ushort.MaxValue );
				ctx.VolumeU += ctx.Heights[i] - next;
				ctx.Heights[i] = next;

				if ( !ctx.Paint || bowlWeight < 0.05f || ctx.Control == null )
					continue;

				Paint( ref ctx, i, bowlWeight );
			}
		}
	}

	static void Paint( ref Context ctx, int i, float weight )
	{
		var control = ctx.Control;
		if ( control == null )
			return;

		var mat = new CompactTerrainMaterial( control[i] );
		if ( mat.IsHole )
			return;

		mat.OverlayTextureId = (byte)ctx.PaintIndex;
		var add = (int)(weight * ctx.PaintStrength * 255f);
		mat.BlendFactor = (byte)Math.Clamp( mat.BlendFactor + add, 0, 255 );
		control[i] = mat.Packed;
		ctx.Painted = true;
	}

	static float BowlWeight( float t, float bowlFrac, bool flatFloor )
	{
		if ( flatFloor )
		{
			if ( t <= 0.5f )
				return 1f;

			var u = Math.Clamp( (t - 0.5f) / 0.5f, 0f, 1f );
			var s = 1f - u;
			return s * s;
		}

		var bowlT = Math.Clamp( t / bowlFrac, 0f, 1f );
		var s2 = 1f - bowlT * bowlT;
		return s2 * s2;
	}

	public static float SmoothMax( float a, float b, float k )
	{
		if ( k <= 0.001f )
			return MathF.Max( a, b );

		var h = Math.Clamp( 0.5f + 0.5f * (a - b) / k, 0f, 1f );
		return b + (a - b) * h + k * h * (1f - h);
	}

	public static float SignedFbm( int octaves, float x, float y )
	{
		return Math.Clamp( Noise.Fbm( octaves, x, y ) * 2f - 1f, -1f, 1f );
	}

	public static float RimWeight( float t, float bowlFrac )
	{
		if ( t < bowlFrac * 0.85f || t >= 1f )
			return 0f;

		var u = (t - bowlFrac * 0.85f) / (1f - bowlFrac * 0.85f);
		return MathF.Sin( u * MathF.PI );
	}
}