Editor/InteriorLayoutBuilder/RoomLayoutGeometryBuilder.Mesh.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Sandbox;

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutGeometryBuilder
{
	private const string PluginFallbackMaterialPath = "materials/room_layout/fallback_neutral.vmat";
	private const string InMemoryFallbackShaderPath = "shaders/complex.shader";

	private static readonly Dictionary<string, Material> MaterialCache = new( StringComparer.OrdinalIgnoreCase );
	private static readonly Dictionary<RoomLayoutSurface, Material> FallbackMaterialCache = new();

	internal static void InvalidateMaterial( string path )
	{
		if ( string.IsNullOrWhiteSpace( path ) )
		{
			return;
		}

		var normalizedPath = NormalizeAssetPath( path );
		MaterialCache.Remove( normalizedPath );
		if ( normalizedPath.EndsWith( ".vmat", StringComparison.OrdinalIgnoreCase ) )
		{
			MaterialCache.Remove( $"{normalizedPath}_c" );
		}
	}

	private static GameObject CreateBox(
		Scene scene,
		GameObject parent,
		string name,
		Vector3 center,
		Vector3 size,
		RoomLayoutSettings settings,
		RoomLayoutSurface surface,
		string materialPath = null,
		float textureWorldSize = 0.0f )
	{
		var gameObject = CreateVisualBox( scene, parent, name, center, size, settings, surface, materialPath, textureWorldSize );

		var collider = gameObject.Components.Create<BoxCollider>();
		collider.Scale = size;
		collider.Static = true;

		return gameObject;
	}

	private static GameObject CreateVisualBox(
		Scene scene,
		GameObject parent,
		string name,
		Vector3 center,
		Vector3 size,
		RoomLayoutSettings settings,
		RoomLayoutSurface surface,
		string materialPath = null,
		float textureWorldSize = 0.0f )
	{
		var gameObject = scene.CreateObject( true );
		gameObject.Name = name;
		gameObject.SetParent( parent, false );
		gameObject.LocalPosition = center;

		var meshComponent = gameObject.Components.Create<MeshComponent>( false );
		meshComponent.Mesh = BuildBoxMesh(
			size,
			center,
			MaterialScaleOrFallback( textureWorldSize, MaterialScaleForSettings( settings, surface ) ),
			MaterialFor( settings, surface, materialPath ) );
		meshComponent.Color = Color.White;
		meshComponent.SmoothingAngle = 0.0f;
		meshComponent.Enabled = true;
		meshComponent.RebuildMesh();

		return gameObject;
	}

	private static void AddUniqueCoordinate( List<float> values, float value )
	{
		foreach ( var existing in values )
		{
			if ( existing.AlmostEqual( value ) )
			{
				return;
			}
		}

		values.Add( value );
	}

	private static int QuantizeCoordinate( float value )
	{
		return (int)MathF.Round( value * 1000.0f );
	}

	private static PolygonMesh BuildBoxMesh(
		Vector3 size,
		Vector3 worldCenter,
		float textureWorldSize,
		Material material )
	{
		var half = size * 0.5f;
		var mesh = new PolygonMesh();
		var vertices = mesh.AddVertices( new[]
		{
			new Vector3( -half.x, -half.y, -half.z ),
			new Vector3( half.x, -half.y, -half.z ),
			new Vector3( half.x, half.y, -half.z ),
			new Vector3( -half.x, half.y, -half.z ),
			new Vector3( -half.x, -half.y, half.z ),
			new Vector3( half.x, -half.y, half.z ),
			new Vector3( half.x, half.y, half.z ),
			new Vector3( -half.x, half.y, half.z )
		} );

		AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[4], vertices[5], vertices[6], vertices[7]], BoxUvPlane.XY );
		AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[0], vertices[3], vertices[2], vertices[1]], BoxUvPlane.XY );
		AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[3], vertices[7], vertices[6], vertices[2]], BoxUvPlane.XZ );
		AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[0], vertices[1], vertices[5], vertices[4]], BoxUvPlane.XZ );
		AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[1], vertices[2], vertices[6], vertices[5]], BoxUvPlane.YZ );
		AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[0], vertices[4], vertices[7], vertices[3]], BoxUvPlane.YZ );

		mesh.ComputeFaceTextureParametersFromCoordinates();
		mesh.SetSmoothingAngle( 0.0f );
		return mesh;
	}

	private static void AddBoxFace(
		PolygonMesh mesh,
		Material material,
		Vector3 worldCenter,
		float textureWorldSize,
		HalfEdgeMesh.VertexHandle[] vertices,
		BoxUvPlane uvPlane )
	{
		var face = mesh.AddFace( vertices );
		mesh.SetFaceMaterial( face, material );

		var textureSize = MathF.Max( 1.0f, textureWorldSize );
		var uvs = new Vector2[vertices.Length];
		for ( var i = 0; i < vertices.Length; i++ )
		{
			var worldPosition = worldCenter + mesh.GetVertexPosition( vertices[i] );
			uvs[i] = ProjectBoxUv( worldPosition, textureSize, uvPlane );
		}

		mesh.SetFaceTextureCoords( face, uvs );
	}

	private static Vector2 ProjectBoxUv( Vector3 worldPosition, float textureWorldSize, BoxUvPlane uvPlane )
	{
		return uvPlane switch
		{
			BoxUvPlane.XY => new Vector2( worldPosition.x, worldPosition.y ) / textureWorldSize,
			BoxUvPlane.XZ => new Vector2( worldPosition.x, worldPosition.z ) / textureWorldSize,
			_ => new Vector2( worldPosition.y, worldPosition.z ) / textureWorldSize
		};
	}

	private enum BoxUvPlane
	{
		XY,
		XZ,
		YZ
	}

	private static Material MaterialFor( RoomLayoutSettings settings, RoomLayoutSurface surface, string materialPath = null )
	{
		var path = !string.IsNullOrWhiteSpace( materialPath )
			? materialPath
			: MaterialPathForSettings( settings, surface );
		path = NormalizeAssetPath( path );

		if ( string.IsNullOrWhiteSpace( path ) || IsKnownMissingBuiltInMaterial( path ) )
		{
			return FallbackMaterialFor( surface );
		}

		if ( IsTexturePath( path ) )
		{
			path = TryCreateTextureMaterial( path );
			if ( string.IsNullOrWhiteSpace( path ) )
			{
				return FallbackMaterialFor( surface );
			}
		}

		if ( !MaterialCache.TryGetValue( path, out var material ) )
		{
			material = Material.Load( path );
			MaterialCache[path] = material;
		}

		return material;
	}

	private static Material FallbackMaterialFor( RoomLayoutSurface surface )
	{
		if ( !FallbackMaterialCache.TryGetValue( surface, out var material ) )
		{
			material = LoadPluginFallbackMaterial() ??
				Material.Create( $"roomlayout_fallback_{surface.ToString().ToLowerInvariant()}", InMemoryFallbackShaderPath );
			FallbackMaterialCache[surface] = material;
		}

		return material;
	}

	private static Material LoadPluginFallbackMaterial()
	{
		if ( !PluginFallbackMaterialExists() )
		{
			return null;
		}

		if ( !MaterialCache.TryGetValue( PluginFallbackMaterialPath, out var material ) )
		{
			material = Material.Load( PluginFallbackMaterialPath );
			MaterialCache[PluginFallbackMaterialPath] = material;
		}

		return material is not null && material.IsValid()
			? material
			: null;
	}

	private static bool PluginFallbackMaterialExists()
	{
		try
		{
			return global::Editor.AssetSystem.FindByPath( PluginFallbackMaterialPath ) is not null ||
				Sandbox.FileSystem.Mounted.FileExists( PluginFallbackMaterialPath ) ||
				Sandbox.FileSystem.Mounted.FileExists( $"{PluginFallbackMaterialPath}_c" );
		}
		catch
		{
			return false;
		}
	}

	private static string MaterialPathForSettings( RoomLayoutSettings settings, RoomLayoutSurface surface )
	{
		return surface switch
		{
			RoomLayoutSurface.Floor => settings.FloorMaterialPath,
			RoomLayoutSurface.Wall => FirstMaterialPath( settings.OuterWallMaterialPath, settings.WallMaterialPath ),
			RoomLayoutSurface.Cap => settings.WallCapMaterialPath,
			RoomLayoutSurface.Baseboard => settings.BaseboardMaterialPath,
			RoomLayoutSurface.DoorFrame => settings.DoorFrameMaterialPath,
			RoomLayoutSurface.WindowFrame => settings.WindowFrameMaterialPath,
			RoomLayoutSurface.CorridorFloor => settings.CorridorFloorMaterialPath,
			RoomLayoutSurface.Threshold => settings.ThresholdMaterialPath,
			RoomLayoutSurface.Roof => settings.RoofMaterialPath,
			_ => settings.WallMaterialPath
		};
	}

	private static float MaterialScaleForSettings( RoomLayoutSettings settings, RoomLayoutSurface surface )
	{
		return surface switch
		{
			RoomLayoutSurface.Floor => MaterialScaleOrFallback( settings.FloorMaterialScale, settings.TextureWorldSize ),
			RoomLayoutSurface.Wall => MaterialScaleOrFallback( settings.OuterWallMaterialScale, MaterialScaleOrFallback( settings.WallMaterialScale, settings.TextureWorldSize ) ),
			RoomLayoutSurface.Cap => MaterialScaleOrFallback( settings.WallCapMaterialScale, settings.TextureWorldSize ),
			RoomLayoutSurface.Baseboard => MaterialScaleOrFallback( settings.BaseboardMaterialScale, settings.TextureWorldSize ),
			RoomLayoutSurface.DoorFrame => MaterialScaleOrFallback( settings.DoorFrameMaterialScale, settings.TextureWorldSize ),
			RoomLayoutSurface.WindowFrame => MaterialScaleOrFallback( settings.WindowFrameMaterialScale, settings.TextureWorldSize ),
			RoomLayoutSurface.CorridorFloor => MaterialScaleOrFallback( settings.CorridorFloorMaterialScale, settings.TextureWorldSize ),
			RoomLayoutSurface.Threshold => MaterialScaleOrFallback( settings.ThresholdMaterialScale, settings.TextureWorldSize ),
			RoomLayoutSurface.Roof => MaterialScaleOrFallback( settings.RoofMaterialScale, settings.TextureWorldSize ),
			_ => MaterialScaleOrFallback( 0.0f, settings.TextureWorldSize )
		};
	}

	private static string RoomMaterialPath( RoomLayoutSettings settings, RoomLayoutRoom room, RoomLayoutSurface surface )
	{
		return surface switch
		{
			RoomLayoutSurface.Floor => FirstMaterialPath( room.FloorMaterialPath, settings.FloorMaterialPath ),
			RoomLayoutSurface.Wall => RoomOuterWallMaterialPath( settings, room ),
			RoomLayoutSurface.Cap => FirstMaterialPath( room.WallCapMaterialPath, settings.WallCapMaterialPath ),
			RoomLayoutSurface.Baseboard => FirstMaterialPath( room.BaseboardMaterialPath, settings.BaseboardMaterialPath ),
			RoomLayoutSurface.Threshold => FirstMaterialPath( room.ThresholdMaterialPath, settings.ThresholdMaterialPath ),
			RoomLayoutSurface.Roof => FirstMaterialPath( room.RoofMaterialPath, settings.RoofMaterialPath ),
			_ => ""
		};
	}

	private static float RoomMaterialScale( RoomLayoutSettings settings, RoomLayoutRoom room, RoomLayoutSurface surface )
	{
		return surface switch
		{
			RoomLayoutSurface.Floor => MaterialScaleOrFallback( room.FloorMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Floor ) ),
			RoomLayoutSurface.Wall => RoomOuterWallMaterialScale( settings, room ),
			RoomLayoutSurface.Cap => MaterialScaleOrFallback( room.WallCapMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Cap ) ),
			RoomLayoutSurface.Baseboard => MaterialScaleOrFallback( room.BaseboardMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Baseboard ) ),
			RoomLayoutSurface.Threshold => MaterialScaleOrFallback( room.ThresholdMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Threshold ) ),
			RoomLayoutSurface.Roof => MaterialScaleOrFallback( room.RoofMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Roof ) ),
			_ => MaterialScaleForSettings( settings, surface )
		};
	}

	private static string CorridorMaterialPath( RoomLayoutSettings settings, RoomLayoutCorridor corridor, RoomLayoutSurface surface )
	{
		return surface switch
		{
			RoomLayoutSurface.Floor or RoomLayoutSurface.CorridorFloor => FirstMaterialPath( corridor.FloorMaterialPath, settings.CorridorFloorMaterialPath ),
			RoomLayoutSurface.Wall => CorridorOuterWallMaterialPath( settings, corridor ),
			RoomLayoutSurface.Cap => FirstMaterialPath( corridor.WallCapMaterialPath, settings.WallCapMaterialPath ),
			RoomLayoutSurface.Baseboard => FirstMaterialPath( corridor.BaseboardMaterialPath, settings.BaseboardMaterialPath ),
			RoomLayoutSurface.Threshold => FirstMaterialPath( corridor.ThresholdMaterialPath, settings.ThresholdMaterialPath ),
			RoomLayoutSurface.Roof => FirstMaterialPath( corridor.RoofMaterialPath, settings.RoofMaterialPath ),
			_ => ""
		};
	}

	private static float CorridorMaterialScale( RoomLayoutSettings settings, RoomLayoutCorridor corridor, RoomLayoutSurface surface )
	{
		return surface switch
		{
			RoomLayoutSurface.Floor or RoomLayoutSurface.CorridorFloor => MaterialScaleOrFallback( corridor.FloorMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.CorridorFloor ) ),
			RoomLayoutSurface.Wall => CorridorOuterWallMaterialScale( settings, corridor ),
			RoomLayoutSurface.Cap => MaterialScaleOrFallback( corridor.WallCapMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Cap ) ),
			RoomLayoutSurface.Baseboard => MaterialScaleOrFallback( corridor.BaseboardMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Baseboard ) ),
			RoomLayoutSurface.Threshold => MaterialScaleOrFallback( corridor.ThresholdMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Threshold ) ),
			RoomLayoutSurface.Roof => MaterialScaleOrFallback( corridor.RoofMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Roof ) ),
			_ => MaterialScaleForSettings( settings, surface )
		};
	}

	private static string RoomOuterWallMaterialPath( RoomLayoutSettings settings, RoomLayoutRoom room )
	{
		return FirstMaterialPath(
			room.OuterWallMaterialPath,
			FirstMaterialPath( room.WallMaterialPath, FirstMaterialPath( settings.OuterWallMaterialPath, settings.WallMaterialPath ) ) );
	}

	private static float RoomOuterWallMaterialScale( RoomLayoutSettings settings, RoomLayoutRoom room )
	{
		return MaterialScaleOrFallback(
			room.OuterWallMaterialScale,
			MaterialScaleOrFallback(
				room.WallMaterialScale,
				MaterialScaleOrFallback(
					settings.OuterWallMaterialScale,
					MaterialScaleOrFallback( settings.WallMaterialScale, settings.TextureWorldSize ) ) ) );
	}

	private static string RoomInnerWallMaterialPath( RoomLayoutSettings settings, RoomLayoutRoom room )
	{
		return FirstMaterialPath(
			room.InnerWallMaterialPath,
			FirstMaterialPath( settings.InnerWallMaterialPath, settings.WallMaterialPath ) );
	}

	private static float RoomInnerWallMaterialScale( RoomLayoutSettings settings, RoomLayoutRoom room )
	{
		return MaterialScaleOrFallback(
			room.InnerWallMaterialScale,
			MaterialScaleOrFallback(
				settings.InnerWallMaterialScale,
				MaterialScaleOrFallback( settings.WallMaterialScale, settings.TextureWorldSize ) ) );
	}

	private static string CorridorOuterWallMaterialPath( RoomLayoutSettings settings, RoomLayoutCorridor corridor )
	{
		return FirstMaterialPath(
			corridor.OuterWallMaterialPath,
			FirstMaterialPath( corridor.WallMaterialPath, FirstMaterialPath( settings.OuterWallMaterialPath, settings.WallMaterialPath ) ) );
	}

	private static float CorridorOuterWallMaterialScale( RoomLayoutSettings settings, RoomLayoutCorridor corridor )
	{
		return MaterialScaleOrFallback(
			corridor.OuterWallMaterialScale,
			MaterialScaleOrFallback(
				corridor.WallMaterialScale,
				MaterialScaleOrFallback(
					settings.OuterWallMaterialScale,
					MaterialScaleOrFallback( settings.WallMaterialScale, settings.TextureWorldSize ) ) ) );
	}

	private static string CorridorInnerWallMaterialPath( RoomLayoutSettings settings, RoomLayoutCorridor corridor )
	{
		return FirstMaterialPath(
			corridor.InnerWallMaterialPath,
			FirstMaterialPath( settings.InnerWallMaterialPath, settings.WallMaterialPath ) );
	}

	private static float CorridorInnerWallMaterialScale( RoomLayoutSettings settings, RoomLayoutCorridor corridor )
	{
		return MaterialScaleOrFallback(
			corridor.InnerWallMaterialScale,
			MaterialScaleOrFallback(
				settings.InnerWallMaterialScale,
				MaterialScaleOrFallback( settings.WallMaterialScale, settings.TextureWorldSize ) ) );
	}

	private static string ThresholdMaterialPath( RoomLayoutDocument document, RoomLayoutCorridor corridor, DoorPoint door )
	{
		var roomMaterialPath = document.FindRoom( door.RoomId ) is { } room
			? room.ThresholdMaterialPath
			: "";

		return FirstMaterialPath(
			corridor.ThresholdMaterialPath,
			FirstMaterialPath( roomMaterialPath, document.Settings.ThresholdMaterialPath ) );
	}

	private static float ThresholdMaterialScale( RoomLayoutDocument document, RoomLayoutCorridor corridor, DoorPoint door )
	{
		var settingsScale = MaterialScaleForSettings( document.Settings, RoomLayoutSurface.Threshold );
		var roomScale = document.FindRoom( door.RoomId ) is { } room
			? room.ThresholdMaterialScale
			: 0.0f;

		return MaterialScaleOrFallback(
			corridor.ThresholdMaterialScale,
			MaterialScaleOrFallback( roomScale, settingsScale ) );
	}

	private static string FirstMaterialPath( string preferred, string fallback )
	{
		if ( !string.IsNullOrWhiteSpace( preferred ) )
		{
			return preferred.Trim();
		}

		return fallback?.Trim() ?? "";
	}

	private static float MaterialScaleOrFallback( float scale, float fallback )
	{
		return Math.Clamp( scale > 0.0f ? scale : fallback, 1.0f, 8192.0f );
	}

	private static bool IsKnownMissingBuiltInMaterial( string path )
	{
		return path.Equals( "materials/dev/gray_50.vmat", StringComparison.OrdinalIgnoreCase ) ||
			path.Equals( "materials/dev/gray_75.vmat", StringComparison.OrdinalIgnoreCase ) ||
			path.Equals( "materials/dev/reflectivity_30.vmat", StringComparison.OrdinalIgnoreCase );
	}

	private static bool IsTexturePath( string path )
	{
		var extension = System.IO.Path.GetExtension( path ).ToLowerInvariant();
		return extension is ".png" or ".jpg" or ".jpeg" or ".tga" or ".psd" or ".vtex";
	}

	private static string TryCreateTextureMaterial( string texturePath )
	{
		var textureAssetPath = NormalizeAssetPath( texturePath );
		var materialPath = $"room-layouts/generated-materials/texture-{StablePathHash( textureAssetPath )}.vmat";

		try
		{
			if ( global::Editor.FileSystem.Content.IsReadOnly )
			{
				return null;
			}

			global::Editor.FileSystem.Content.CreateDirectory( "room-layouts/generated-materials" );
			global::Editor.FileSystem.Content.WriteAllText( materialPath, BuildTextureMaterialFile( textureAssetPath ) );
			return materialPath;
		}
		catch ( Exception exception )
		{
			Log.Warning( $"Unable to create interior layout material for {textureAssetPath}: {exception.Message}" );
			return null;
		}
	}

	private static string NormalizeAssetPath( string path )
	{
		if ( string.IsNullOrWhiteSpace( path ) )
		{
			return "";
		}

		path = path.Trim().Replace( '\\', '/' ).TrimStart( '/' );
		if ( path.StartsWith( "assets/", StringComparison.OrdinalIgnoreCase ) )
		{
			path = path[7..];
		}

		return path.EndsWith( ".vmat_c", StringComparison.OrdinalIgnoreCase )
			? path[..^2]
			: path;
	}

	private static string BuildTextureMaterialFile( string texturePath )
	{
		texturePath = texturePath.Replace( "\"", "\\\"" );
		return $$"""
			"Layer0"
			{
				"shader"		"shaders/complex.shader"
				"g_flModelTintAmount"		"1.000000"
				"g_vColorTint"		"[1.000000 1.000000 1.000000 0.000000]"
				"TextureColor"		"{{texturePath}}"
				"g_flMetalness"		"0.000000"
				"g_flRoughness"		"0.850000"
				"g_bFogEnabled"		"1"
				"g_vTexCoordScale"		"[1.000 1.000]"
			}
			""";
	}

	private static string StablePathHash( string path )
	{
		const uint fnvOffset = 2166136261;
		const uint fnvPrime = 16777619;

		var hash = fnvOffset;
		foreach ( var value in Encoding.UTF8.GetBytes( path.ToLowerInvariant() ) )
		{
			hash ^= value;
			hash *= fnvPrime;
		}

		return hash.ToString( "x8", CultureInfo.InvariantCulture );
	}

	private enum RoomLayoutSurface
	{
		Floor,
		Wall,
		Cap,
		Baseboard,
		DoorFrame,
		WindowFrame,
		CorridorFloor,
		Threshold,
		Roof
	}
}