Editor/SeamlessImageProcessor.cs
using System;
using Sandbox;

public static class SeamlessImageProcessor
{
	public static Bitmap Process( Bitmap source, SeamlessToolSettings settings )
	{
		if ( source == null || !source.IsValid )
			return null;

		settings ??= new SeamlessToolSettings();

		var width = source.Width;
		var height = source.Height;

		if ( width <= 0 || height <= 0 )
			return null;

		var sourcePixels = source.GetPixels();
		var offsetBlendPixels = CreateOffsetBlendPixels( sourcePixels, width, height, settings );

		var outputPixels = settings.ProcessingMode switch
		{
			SeamlessProcessingMode.IrregularPatch => ProcessIrregularPatch( sourcePixels, offsetBlendPixels, width, height, settings ),
			SeamlessProcessingMode.TextureBombing => ProcessTextureBombing( sourcePixels, offsetBlendPixels, width, height, settings ),
			SeamlessProcessingMode.BrickAndTile => ProcessBrickAndTile( sourcePixels, width, height, settings ),
			_ => offsetBlendPixels
		};

		var result = new Bitmap( width, height, source.IsFloatingPoint );
		result.SetPixels( outputPixels );

		if ( settings.BlurRadius > 0.01f )
		{
			result.Blur( settings.BlurRadius, true );
		}

		if ( settings.SharpenAmount > 0.01f )
		{
			result.Sharpen( settings.SharpenAmount, true );
		}

		return result;
	}

	private static Color[] CreateOffsetBlendPixels( Color[] sourcePixels, int width, int height, SeamlessToolSettings settings )
	{
		var halfWidth = width / 2;
		var halfHeight = height / 2;
		var pixels = CreateOffsetPixels( sourcePixels, width, height, halfWidth, halfHeight );

		BlendVerticalSeam( pixels, width, height, settings );
		BlendHorizontalSeam( pixels, width, height, settings );

		return pixels;
	}

	private static Color[] CreateOffsetPixels( Color[] sourcePixels, int width, int height, int offsetX, int offsetY )
	{
		var pixels = new Color[sourcePixels.Length];

		for ( var y = 0; y < height; y++ )
		{
			var sourceY = WrapIndex( y + offsetY, height );

			for ( var x = 0; x < width; x++ )
			{
				var sourceX = WrapIndex( x + offsetX, width );
				pixels[y * width + x] = sourcePixels[sourceY * width + sourceX];
			}
		}

		return pixels;
	}

	private static Color[] ProcessBrickAndTile( Color[] sourcePixels, int width, int height, SeamlessToolSettings settings )
	{
		var offsetX = GetCellAlignedOffset( width, settings.BrickTileCellWidth );
		var offsetY = GetCellAlignedOffset( height, settings.BrickTileCellHeight );
		var pixels = CreateOffsetPixels( sourcePixels, width, height, offsetX, offsetY );
		var seamX = GetSeamCenterFromOffset( width, offsetX );
		var seamY = GetSeamCenterFromOffset( height, offsetY );

		BlendBrickTileVerticalSeam( pixels, width, height, seamX, settings );
		BlendBrickTileHorizontalSeam( pixels, width, height, seamY, settings );

		return pixels;
	}

	private static Color[] ProcessIrregularPatch( Color[] sourcePixels, Color[] basePixels, int width, int height, SeamlessToolSettings settings )
	{
		var result = (Color[])basePixels.Clone();
		var gridX = GetPatchGridX( width, height, settings.PatchCount );
		var gridY = GetPatchGridY( gridX, settings.PatchCount );
		var strength = Clamp01( settings.VariationStrength * GetVariationMultiplier( settings.VariationPreset ) );

		for ( var y = 0; y < height; y++ )
		{
			var gridYPosition = (y + 0.5f) / Math.Max( 1, height ) * gridY;
			var cellY = WrapIndex( (int)MathF.Floor( gridYPosition ), gridY );
			var localY = gridYPosition - MathF.Floor( gridYPosition );
			var blendY = SmoothFade( localY );

			for ( var x = 0; x < width; x++ )
			{
				var gridXPosition = (x + 0.5f) / Math.Max( 1, width ) * gridX;
				var cellX = WrapIndex( (int)MathF.Floor( gridXPosition ), gridX );
				var localX = gridXPosition - MathF.Floor( gridXPosition );
				var blendX = SmoothFade( localX );

				var c00 = SampleWangCell( sourcePixels, width, height, x, y, cellX, cellY, gridX, gridY, settings );
				var c10 = SampleWangCell( sourcePixels, width, height, x, y, cellX + 1, cellY, gridX, gridY, settings );
				var c01 = SampleWangCell( sourcePixels, width, height, x, y, cellX, cellY + 1, gridX, gridY, settings );
				var c11 = SampleWangCell( sourcePixels, width, height, x, y, cellX + 1, cellY + 1, gridX, gridY, settings );

				var top = Color.Lerp( c00, c10, blendX, true );
				var bottom = Color.Lerp( c01, c11, blendX, true );
				var wangSample = Color.Lerp( top, bottom, blendY, true );

				var mask = GetIrregularPatchMask( localX, localY, x, y, width, height, settings );
				var amount = Clamp01( strength * mask );
				var index = y * width + x;

				if ( amount <= 0.001f )
					continue;

				result[index] = Color.Lerp( basePixels[index], wangSample, amount, true );
			}
		}

		return result;
	}

	private static Color[] ProcessTextureBombing( Color[] sourcePixels, Color[] basePixels, int width, int height, SeamlessToolSettings settings )
	{
		var result = (Color[])basePixels.Clone();
		var pixelCount = result.Length;
		var red = new float[pixelCount];
		var green = new float[pixelCount];
		var blue = new float[pixelCount];
		var alpha = new float[pixelCount];
		var weights = new float[pixelCount];

		var random = new Random( settings.RandomSeed );
		var patchRadius = Math.Clamp( settings.PatchSize * 0.5f, 4f, Math.Max( width, height ) );
		var density = Math.Clamp( settings.TextureBombDensity, 0.1f, 4f );
		var bombCount = Math.Clamp( (int)MathF.Ceiling( settings.PatchCount * density ), 1, 2048 );
		var strength = Clamp01( settings.VariationStrength * GetVariationMultiplier( settings.VariationPreset ) );
		var allowTransforms = settings.VariationPreset != SeamlessVariationPreset.Gentle;

		for ( var i = 0; i < bombCount; i++ )
		{
			GetTextureBombCenter( random, width, height, settings.SeamGravity, out var centerX, out var centerY );
			var sampleCenterX = random.Next( width );
			var sampleCenterY = random.Next( height );
			var radius = patchRadius * Lerp( 0.65f, 1.35f, (float)random.NextDouble() );
			var radiusInt = Math.Max( 2, (int)MathF.Ceiling( radius ) );
			var rotationSteps = allowTransforms ? random.Next( 4 ) : 0;
			var flipX = allowTransforms && random.NextDouble() > 0.5;
			var flipY = settings.VariationPreset == SeamlessVariationPreset.Strong && random.NextDouble() > 0.5;
			var noiseSeed = random.Next();

			for ( var localY = -radiusInt; localY <= radiusInt; localY++ )
			{
				for ( var localX = -radiusInt; localX <= radiusInt; localX++ )
				{
					var distance = GetPatchShapeDistance( localX, localY, radius, settings.PatchShape );

					if ( distance >= 1f )
						continue;

					var destX = WrapIndex( centerX + localX, width );
					var destY = WrapIndex( centerY + localY, height );
					var mask = GetSoftPatchMask( 1f - distance, settings.MaskSoftness );
					var noise = Hash01( destX, destY, noiseSeed );
					mask *= Lerp( 0.7f, 1f, noise );
					mask = ApplySeamOverlayPressure( mask, destX, destY, width, height, settings );

					if ( mask <= 0.001f )
						continue;

					GetTransformedRelativePosition( localX, localY, rotationSteps, flipX, flipY, out var sampleXOffset, out var sampleYOffset );

					var sourceX = WrapIndex( sampleCenterX + sampleXOffset, width );
					var sourceY = WrapIndex( sampleCenterY + sampleYOffset, height );
					var color = sourcePixels[sourceY * width + sourceX];
					var index = destY * width + destX;

					red[index] += color.r * mask;
					green[index] += color.g * mask;
					blue[index] += color.b * mask;
					alpha[index] += color.a * mask;
					weights[index] += mask;
				}
			}
		}

		for ( var i = 0; i < result.Length; i++ )
		{
			var weight = weights[i];

			if ( weight <= 0.001f )
				continue;

			var bombed = new Color( red[i] / weight, green[i] / weight, blue[i] / weight, alpha[i] / weight );
			var amount = Clamp01( strength * weight / (weight + 1f) );
			result[i] = Color.Lerp( result[i], bombed, amount, true );
		}

		return result;
	}

	private static void BlendVerticalSeam( Color[] pixels, int width, int height, SeamlessToolSettings settings )
	{
		if ( width < 2 || settings.SeamWidth <= 0f || settings.BlendStrength <= 0f )
			return;

		var source = (Color[])pixels.Clone();
		var seamCenter = width / 2;
		var maxDistance = MathF.Min( settings.SeamWidth, width * 0.5f );
		var startX = Math.Max( 0, seamCenter - (int)MathF.Ceiling( maxDistance ) );
		var endX = Math.Min( width - 1, seamCenter + (int)MathF.Ceiling( maxDistance ) - 1 );

		for ( var y = 0; y < height; y++ )
		{
			for ( var x = startX; x <= endX; x++ )
			{
				var distance = MathF.Abs( (x + 0.5f) - seamCenter );
				var baseWeight = 1f - Clamp01( distance / maxDistance );
				var weight = GetSeamWeight( baseWeight, x, y, width, height, settings );
				var amount = Clamp01( weight * settings.BlendStrength ) * 0.5f;

				if ( amount <= 0f )
					continue;

				var mirrorX = MirrorAcrossCenterSeam( x, seamCenter, width );
				var index = y * width + x;
				var mirrorIndex = y * width + mirrorX;
				pixels[index] = Color.Lerp( source[index], source[mirrorIndex], amount, true );
			}
		}
	}

	private static void BlendHorizontalSeam( Color[] pixels, int width, int height, SeamlessToolSettings settings )
	{
		if ( height < 2 || settings.SeamWidth <= 0f || settings.BlendStrength <= 0f )
			return;

		var source = (Color[])pixels.Clone();
		var seamCenter = height / 2;
		var maxDistance = MathF.Min( settings.SeamWidth, height * 0.5f );
		var startY = Math.Max( 0, seamCenter - (int)MathF.Ceiling( maxDistance ) );
		var endY = Math.Min( height - 1, seamCenter + (int)MathF.Ceiling( maxDistance ) - 1 );

		for ( var y = startY; y <= endY; y++ )
		{
			var distance = MathF.Abs( (y + 0.5f) - seamCenter );
			var baseWeight = 1f - Clamp01( distance / maxDistance );

			for ( var x = 0; x < width; x++ )
			{
				var weight = GetSeamWeight( baseWeight, x, y, width, height, settings );
				var amount = Clamp01( weight * settings.BlendStrength ) * 0.5f;

				if ( amount <= 0f )
					continue;

				var mirrorY = MirrorAcrossCenterSeam( y, seamCenter, height );
				var index = y * width + x;
				var mirrorIndex = mirrorY * width + x;
				pixels[index] = Color.Lerp( source[index], source[mirrorIndex], amount, true );
			}
		}
	}

	private static void BlendBrickTileVerticalSeam( Color[] pixels, int width, int height, int seamCenter, SeamlessToolSettings settings )
	{
		if ( width < 2 || settings.SeamWidth <= 0f || settings.BlendStrength <= 0f )
			return;

		var source = (Color[])pixels.Clone();
		var maxDistance = MathF.Min( settings.SeamWidth, width * 0.5f );
		var startX = Math.Max( 0, seamCenter - (int)MathF.Ceiling( maxDistance ) );
		var endX = Math.Min( width - 1, seamCenter + (int)MathF.Ceiling( maxDistance ) - 1 );

		for ( var y = 0; y < height; y++ )
		{
			for ( var x = startX; x <= endX; x++ )
			{
				var distance = MathF.Abs( (x + 0.5f) - seamCenter );
				var baseWeight = 1f - Clamp01( distance / maxDistance );
				var amount = GetBrickTileSeamAmount( baseWeight, x, y, width, height, settings );

				if ( amount <= 0f )
					continue;

				var mirrorX = MirrorAcrossCenterSeam( x, seamCenter, width );
				var index = y * width + x;
				var mirrorIndex = y * width + mirrorX;
				pixels[index] = Color.Lerp( source[index], source[mirrorIndex], amount, true );
			}
		}
	}

	private static void BlendBrickTileHorizontalSeam( Color[] pixels, int width, int height, int seamCenter, SeamlessToolSettings settings )
	{
		if ( height < 2 || settings.SeamWidth <= 0f || settings.BlendStrength <= 0f )
			return;

		var source = (Color[])pixels.Clone();
		var maxDistance = MathF.Min( settings.SeamWidth, height * 0.5f );
		var startY = Math.Max( 0, seamCenter - (int)MathF.Ceiling( maxDistance ) );
		var endY = Math.Min( height - 1, seamCenter + (int)MathF.Ceiling( maxDistance ) - 1 );

		for ( var y = startY; y <= endY; y++ )
		{
			var distance = MathF.Abs( (y + 0.5f) - seamCenter );
			var baseWeight = 1f - Clamp01( distance / maxDistance );

			for ( var x = 0; x < width; x++ )
			{
				var amount = GetBrickTileSeamAmount( baseWeight, x, y, width, height, settings );

				if ( amount <= 0f )
					continue;

				var mirrorY = MirrorAcrossCenterSeam( y, seamCenter, height );
				var index = y * width + x;
				var mirrorIndex = mirrorY * width + x;
				pixels[index] = Color.Lerp( source[index], source[mirrorIndex], amount, true );
			}
		}
	}

	private static Color SampleWangCell( Color[] sourcePixels, int width, int height, int x, int y, int cellX, int cellY, int gridX, int gridY, SeamlessToolSettings settings )
	{
		var hash = GetWangCellHash( cellX, cellY, gridX, gridY, settings.RandomSeed );
		var offsetX = (int)MathF.Round( Hash01( hash, 17, settings.RandomSeed ) * width );
		var offsetY = (int)MathF.Round( Hash01( hash, 31, settings.RandomSeed ) * height );
		var sourceX = WrapIndex( x + offsetX, width );
		var sourceY = WrapIndex( y + offsetY, height );

		return sourcePixels[sourceY * width + sourceX];
	}

	private static int GetWangCellHash( int cellX, int cellY, int gridX, int gridY, int seed )
	{
		cellX = WrapIndex( cellX, gridX );
		cellY = WrapIndex( cellY, gridY );

		var left = GetVerticalEdgeColor( cellX, cellY, gridX, gridY, seed );
		var right = GetVerticalEdgeColor( cellX + 1, cellY, gridX, gridY, seed );
		var top = GetHorizontalEdgeColor( cellX, cellY, gridX, gridY, seed );
		var bottom = GetHorizontalEdgeColor( cellX, cellY + 1, gridX, gridY, seed );

		return HashInt( left, right, top, bottom, seed );
	}

	private static int GetVerticalEdgeColor( int edgeX, int cellY, int gridX, int gridY, int seed )
	{
		return Math.Min( 3, (int)MathF.Floor( Hash01( WrapIndex( edgeX, gridX ), WrapIndex( cellY, gridY ), seed + 101 ) * 4f ) );
	}

	private static int GetHorizontalEdgeColor( int cellX, int edgeY, int gridX, int gridY, int seed )
	{
		return Math.Min( 3, (int)MathF.Floor( Hash01( WrapIndex( cellX, gridX ), WrapIndex( edgeY, gridY ), seed + 211 ) * 4f ) );
	}

	private static float GetIrregularPatchMask( float localX, float localY, int x, int y, int width, int height, SeamlessToolSettings settings )
	{
		var dx = MathF.Abs( localX - 0.5f ) * 2f;
		var dy = MathF.Abs( localY - 0.5f ) * 2f;
		var centerWeight = 1f - Clamp01( GetPatchShapeDistance( dx, dy, 1f, settings.PatchShape ) );
		var mask = GetSoftPatchMask( centerWeight, settings.MaskSoftness );
		var noiseScale = Math.Max( 4f, settings.PatchSize * 0.25f );
		var noiseX = (int)MathF.Floor( x / noiseScale );
		var noiseY = (int)MathF.Floor( y / noiseScale );
		var noise = Hash01( noiseX, noiseY, settings.RandomSeed + 773 );
		var baseMask = Clamp01( mask * Lerp( 0.65f, 1f, noise ) + 0.25f );
		var gravity = Clamp01( settings.SeamGravity );

		if ( gravity <= 0f )
			return baseMask;

		return ApplySeamOverlayPressure( baseMask, x, y, width, height, settings );
	}

	private static float ApplySeamOverlayPressure( float value, int x, int y, int width, int height, SeamlessToolSettings settings )
	{
		var gravity = Clamp01( settings.SeamGravity );

		if ( gravity <= 0f )
			return value;

		var seamProximity = GetCenterSeamProximity( x, y, width, height, settings );
		var offSeamFalloff = Lerp( 1f, seamProximity, gravity );
		var seamBoost = Lerp( 1f, 2.5f, gravity * seamProximity );

		return value * offSeamFalloff * seamBoost;
	}

	private static void GetTextureBombCenter( Random random, int width, int height, float seamGravity, out int centerX, out int centerY )
	{
		var gravity = Clamp01( seamGravity );

		if ( gravity <= 0f || (float)random.NextDouble() > gravity )
		{
			centerX = random.Next( width );
			centerY = random.Next( height );
			return;
		}

		var useVerticalSeam = random.NextDouble() < 0.5;

		if ( useVerticalSeam )
		{
			centerX = GetSeamBiasedAxisPosition( random, width, gravity );
			centerY = random.Next( height );
			return;
		}

		centerX = random.Next( width );
		centerY = GetSeamBiasedAxisPosition( random, height, gravity );
	}

	private static int GetSeamBiasedAxisPosition( Random random, int size, float gravity )
	{
		if ( size <= 1 )
			return 0;

		var seamCenter = size * 0.5f;
		var maxDistance = size * 0.5f;
		var falloffPower = Lerp( 1f, 4f, gravity );
		var distance = maxDistance * MathF.Pow( (float)random.NextDouble(), falloffPower );
		var direction = random.NextDouble() < 0.5 ? -1f : 1f;
		var position = (int)MathF.Floor( seamCenter + distance * direction );

		return WrapIndex( position, size );
	}

	private static float GetPatchShapeDistance( float x, float y, float radius, SeamlessPatchShape shape )
	{
		var safeRadius = Math.Max( 0.001f, radius );
		var normalizedX = MathF.Abs( x ) / safeRadius;
		var normalizedY = MathF.Abs( y ) / safeRadius;

		return shape switch
		{
			SeamlessPatchShape.Square => MathF.Max( normalizedX, normalizedY ),
			SeamlessPatchShape.Diamond => normalizedX + normalizedY,
			SeamlessPatchShape.Hexagon => GetHexagonDistance( normalizedX, normalizedY ),
			_ => MathF.Sqrt( normalizedX * normalizedX + normalizedY * normalizedY )
		};
	}

	private static float GetHexagonDistance( float normalizedX, float normalizedY )
	{
		var flatSideDistance = normalizedX;
		var angledSideDistance = normalizedX * 0.5f + normalizedY * 0.8660254f;
		return MathF.Max( flatSideDistance, angledSideDistance );
	}

	private static float GetCenterSeamProximity( int x, int y, int width, int height, SeamlessToolSettings settings )
	{
		var verticalDistance = MathF.Abs( (x + 0.5f) - width * 0.5f );
		var horizontalDistance = MathF.Abs( (y + 0.5f) - height * 0.5f );
		var distanceToNearestSeam = MathF.Min( verticalDistance, horizontalDistance );
		var spread = MathF.Max( settings.SeamWidth, settings.PatchSize ) * 1.5f;
		spread = Math.Clamp( spread, 1f, MathF.Max( 1f, MathF.Min( width, height ) * 0.5f ) );

		return SmoothFade( 1f - Clamp01( distanceToNearestSeam / spread ) );
	}

	private static float GetSoftPatchMask( float value, float softness )
	{
		value = Clamp01( value );
		softness = Math.Clamp( softness, 0.05f, 1f );
		var shaped = Clamp01( value / softness );
		return SmoothFade( shaped );
	}

	private static int GetCellAlignedOffset( int size, float cellSize )
	{
		if ( size <= 1 )
			return 0;

		if ( cellSize <= 1f )
			return size / 2;

		var aligned = (int)MathF.Round( (size * 0.5f) / cellSize ) * (int)MathF.Round( cellSize );

		if ( aligned <= 0 )
			aligned = size / 2;

		var wrapped = WrapIndex( aligned, size );
		return wrapped == 0 ? size / 2 : wrapped;
	}

	private static int GetSeamCenterFromOffset( int size, int offset )
	{
		var wrappedOffset = WrapIndex( offset, size );

		if ( wrappedOffset <= 0 )
			return size / 2;

		var seamCenter = size - wrappedOffset;

		if ( seamCenter <= 0 || seamCenter >= size )
			return size / 2;

		return seamCenter;
	}

	private static float GetBrickTileSeamAmount( float baseWeight, int x, int y, int width, int height, SeamlessToolSettings settings )
	{
		var normalAmount = Clamp01( GetSeamWeight( baseWeight, x, y, width, height, settings ) * settings.BlendStrength ) * 0.5f;
		var structureMask = GetBrickTileStructureMask( x, y, settings );
		var structureStrength = Clamp01( settings.BrickTileStructureStrength );
		var structuredAmount = normalAmount * Lerp( 0.35f, 1.2f, structureMask );

		return Clamp01( Lerp( normalAmount, structuredAmount, structureStrength ) );
	}

	private static float GetBrickTileStructureMask( int x, int y, SeamlessToolSettings settings )
	{
		var cellWidth = Math.Max( 4f, settings.BrickTileCellWidth );
		var cellHeight = Math.Max( 4f, settings.BrickTileCellHeight );
		var groutWidth = Math.Clamp( settings.BrickTileGroutWidth, 0f, MathF.Min( cellWidth, cellHeight ) * 0.5f );
		var row = (int)MathF.Floor( y / cellHeight );
		var rowOffset = GetBrickTileRowOffset( row, cellWidth, settings );
		var xDistance = GetDistanceToRepeatLine( x + rowOffset, cellWidth );
		var yDistance = GetDistanceToRepeatLine( y, cellHeight );
		var xMask = GetBrickTileLineMask( xDistance, groutWidth );
		var yMask = GetBrickTileLineMask( yDistance, groutWidth );

		return settings.BrickTilePattern switch
		{
			SeamlessBrickTilePattern.Planks => Math.Max( yMask, xMask * 0.65f ),
			SeamlessBrickTilePattern.Panels => Math.Max( xMask, yMask ),
			_ => Math.Max( xMask, yMask )
		};
	}

	private static float GetBrickTileRowOffset( int row, float cellWidth, SeamlessToolSettings settings )
	{
		var offset = Math.Clamp( settings.BrickTileRowOffset, 0f, 1f ) * cellWidth;

		return settings.BrickTilePattern switch
		{
			SeamlessBrickTilePattern.RunningBrick => row % 2 == 0 ? 0f : offset,
			SeamlessBrickTilePattern.Planks => row % 3 * offset,
			SeamlessBrickTilePattern.Panels => row % 2 == 0 ? 0f : offset * 0.5f,
			_ => 0f
		};
	}

	private static float GetDistanceToRepeatLine( float value, float period )
	{
		if ( period <= 0.001f )
			return 0f;

		var wrapped = value - MathF.Floor( value / period ) * period;
		return MathF.Min( wrapped, period - wrapped );
	}

	private static float GetBrickTileLineMask( float distance, float groutWidth )
	{
		var halfWidth = Math.Max( 0.5f, groutWidth * 0.5f );
		var softness = Math.Max( 1f, halfWidth * 0.5f );

		return SmoothFade( 1f - Clamp01( (distance - halfWidth) / softness ) );
	}

	private static float GetSeamWeight( float baseWeight, int x, int y, int width, int height, SeamlessToolSettings settings )
	{
		baseWeight = Clamp01( baseWeight );

		return settings.SeamShape switch
		{
			SeamlessSeamShape.Linear => baseWeight,
			SeamlessSeamShape.Wavy => Clamp01( SmoothFade( baseWeight ) * (0.75f + 0.25f * MathF.Sin( y * 0.08f + settings.RandomSeed * 0.001f )) ),
			SeamlessSeamShape.Noise => Clamp01( SmoothFade( baseWeight ) * Lerp( 0.55f, 1.15f, Hash01( x / 8, y / 8, settings.RandomSeed ) ) ),
			SeamlessSeamShape.Irregular => GetIrregularSeamWeight( baseWeight, x, y, width, height, settings.RandomSeed ),
			_ => SmoothFade( baseWeight )
		};
	}

	private static float GetIrregularSeamWeight( float baseWeight, int x, int y, int width, int height, int seed )
	{
		var wave = 0.5f + 0.5f * MathF.Sin( (x + y) * 0.045f + seed * 0.002f );
		var noise = Hash01( x / 12, y / 12, seed + 19 );
		var shifted = baseWeight + (noise - 0.5f) * 0.28f + (wave - 0.5f) * 0.18f;
		return SmoothFade( Clamp01( shifted ) );
	}

	private static void GetTransformedRelativePosition( int x, int y, int rotationSteps, bool flipX, bool flipY, out int resultX, out int resultY )
	{
		if ( flipX )
			x = -x;

		if ( flipY )
			y = -y;

		rotationSteps = WrapIndex( rotationSteps, 4 );

		switch ( rotationSteps )
		{
			case 1:
				resultX = -y;
				resultY = x;
				return;
			case 2:
				resultX = -x;
				resultY = -y;
				return;
			case 3:
				resultX = y;
				resultY = -x;
				return;
			default:
				resultX = x;
				resultY = y;
				return;
		}
	}

	private static int GetPatchGridX( int width, int height, int patchCount )
	{
		patchCount = Math.Clamp( patchCount, 2, 64 );
		var aspect = Math.Max( 0.25f, width / (float)Math.Max( 1, height ) );
		return Math.Clamp( (int)MathF.Round( MathF.Sqrt( patchCount * aspect ) ), 2, 16 );
	}

	private static int GetPatchGridY( int gridX, int patchCount )
	{
		patchCount = Math.Clamp( patchCount, 2, 64 );
		return Math.Clamp( (int)MathF.Ceiling( patchCount / (float)Math.Max( 1, gridX ) ), 2, 16 );
	}

	private static float GetVariationMultiplier( SeamlessVariationPreset preset )
	{
		return preset switch
		{
			SeamlessVariationPreset.Gentle => 0.55f,
			SeamlessVariationPreset.Strong => 1.15f,
			_ => 0.85f
		};
	}

	private static int MirrorAcrossCenterSeam( int value, int seamCenter, int size )
	{
		if ( value < seamCenter )
			return Math.Clamp( seamCenter + (seamCenter - 1 - value), 0, size - 1 );

		return Math.Clamp( seamCenter - 1 - (value - seamCenter), 0, size - 1 );
	}

	private static int WrapIndex( int value, int size )
	{
		if ( size <= 0 )
			return 0;

		var wrapped = value % size;
		return wrapped < 0 ? wrapped + size : wrapped;
	}

	private static float SmoothFade( float value )
	{
		value = Clamp01( value );
		return value * value * (3f - 2f * value);
	}

	private static float Lerp( float a, float b, float amount )
	{
		return a + (b - a) * Clamp01( amount );
	}

	private static float Clamp01( float value )
	{
		if ( value < 0f )
			return 0f;

		if ( value > 1f )
			return 1f;

		return value;
	}

	private static float Hash01( int a, int b, int c )
	{
		var hash = HashInt( a, b, c, 374761393, 668265263 );
		return (hash & 0x00FFFFFF) / 16777215f;
	}

	private static int HashInt( int a, int b, int c, int d, int e )
	{
		unchecked
		{
			var hash = 2166136261u;
			hash = (hash ^ (uint)a) * 16777619u;
			hash = (hash ^ (uint)b) * 16777619u;
			hash = (hash ^ (uint)c) * 16777619u;
			hash = (hash ^ (uint)d) * 16777619u;
			hash = (hash ^ (uint)e) * 16777619u;
			hash ^= hash >> 13;
			hash *= 1274126177u;
			hash ^= hash >> 16;
			return (int)(hash & 0x7FFFFFFF);
		}
	}
}