Editor/SeamlessSuiteImageUtility.cs
using System;
using System.Collections.Generic;
using System.IO;
using Editor;
using Sandbox;

public static class SeamlessSuiteImageUtility
{
	private static readonly HashSet<string> SupportedExtensions = new( StringComparer.OrdinalIgnoreCase )
	{
		".png",
		".jpg",
		".jpeg",
		".webp",
		".bmp",
		".tga",
		".tif",
		".tiff",
		".psd",
		".svg"
	};

	public static bool IsSupportedImageFile( string path )
	{
		if ( string.IsNullOrWhiteSpace( path ) )
			return false;

		return SupportedExtensions.Contains( Path.GetExtension( path ) );
	}

	public static string ResolveFilePath( string path )
	{
		if ( string.IsNullOrWhiteSpace( path ) )
			return path;

		if ( File.Exists( path ) )
			return path;

		var asset = AssetSystem.FindByPath( path );

		if ( asset != null && File.Exists( asset.AbsolutePath ) )
			return asset.AbsolutePath;

		return path;
	}

	public static Bitmap LoadBitmapFromFile( string path )
	{
		var extension = Path.GetExtension( path ).ToLowerInvariant();
		var bytes = File.ReadAllBytes( path );

		return extension switch
		{
			".tga" => Bitmap.CreateFromTgaBytes( bytes ),
			".tif" => Bitmap.CreateFromTifBytes( bytes ),
			".tiff" => Bitmap.CreateFromTifBytes( bytes ),
			".psd" => Bitmap.CreateFromPsdBytes( bytes ),
			".svg" => Bitmap.CreateFromSvgString( File.ReadAllText( path ), null, null, null, null, null ),
			_ => Bitmap.CreateFromBytes( bytes )
		};
	}

	public static List<string> GetFilesFromDragData( DragData data )
	{
		var files = new List<string>();

		if ( data?.Files != null )
		{
			foreach ( var file in data.Files )
			{
				files.Add( file );
			}
		}

		if ( data?.Assets != null )
		{
			foreach ( var dragAsset in data.Assets )
			{
				if ( !string.IsNullOrWhiteSpace( dragAsset.AssetPath ) )
					files.Add( dragAsset.AssetPath );
			}
		}

		if ( data != null )
		{
			foreach ( var asset in data.OfType<Editor.Asset>() )
			{
				AddAssetPathCandidates( files, asset );
			}
		}

		return files;
	}

	public static string FindFirstSupportedFile( IEnumerable<string> files )
	{
		foreach ( var file in files )
		{
			var resolved = ResolveFilePath( file );

			if ( IsSupportedImageFile( resolved ) )
				return resolved;
		}

		return null;
	}

	public static string BuildUniquePath( string outputDirectory, string sourceName, string suffix, string extension, bool overwriteExisting )
	{
		var cleanSourceName = CleanFileNamePart( string.IsNullOrWhiteSpace( sourceName ) ? "texture" : sourceName );
		var cleanSuffix = CleanFileNamePart( suffix );
		var basePath = Path.Combine( outputDirectory, $"{cleanSourceName}{cleanSuffix}{extension}" );

		if ( overwriteExisting || !File.Exists( basePath ) )
			return basePath;

		for ( var i = 1; i < 1000; i++ )
		{
			var numberedPath = Path.Combine( outputDirectory, $"{cleanSourceName}{cleanSuffix}_{i:000}{extension}" );

			if ( !File.Exists( numberedPath ) )
				return numberedPath;
		}

		return basePath;
	}

	public static void RegisterFile( string path, Action<string> logMessage )
	{
		try
		{
			AssetSystem.RegisterFile( path );
		}
		catch ( Exception ex )
		{
			logMessage?.Invoke( $"Export wrote the file, but S&box asset registration skipped it: {ex.Message}" );
		}
	}

	public static string GetTextureReferencePath( string texturePath, Action<string> addWarning )
	{
		var fullTexturePath = Path.GetFullPath( texturePath );
		var normalizedTexturePath = NormalizePath( fullTexturePath );
		var projectAssetsPath = Sandbox.Project.Current?.GetAssetsPath();

		if ( !string.IsNullOrWhiteSpace( projectAssetsPath ) )
		{
			var normalizedAssetsPath = NormalizePath( Path.GetFullPath( projectAssetsPath ) );

			if ( !normalizedAssetsPath.EndsWith( "/" ) )
				normalizedAssetsPath += "/";

			if ( normalizedTexturePath.StartsWith( normalizedAssetsPath, StringComparison.OrdinalIgnoreCase ) )
				return normalizedTexturePath.Substring( normalizedAssetsPath.Length );

			addWarning?.Invoke( "The exported texture stack is outside the active project's Assets folder. S&box may not treat it as normal project assets; choose a folder under Assets for best editor use." );
		}
		else
		{
			addWarning?.Invoke( "Could not find the active project's Assets folder. The .vmat will reference textures by full path." );
		}

		return normalizedTexturePath;
	}

	public static string CleanFileNamePart( string value )
	{
		if ( string.IsNullOrEmpty( value ) )
			return "";

		var invalid = Path.GetInvalidFileNameChars();
		var cleaned = value;

		foreach ( var character in invalid )
		{
			cleaned = cleaned.Replace( character, '_' );
		}

		return cleaned;
	}

	public static string NormalizePath( string path )
	{
		return path?.Replace( '\\', '/' ) ?? "";
	}

	public static float GetLuminance( Color color )
	{
		return Clamp01( color.r * 0.2126f + color.g * 0.7152f + color.b * 0.0722f );
	}

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

		if ( value > 1f )
			return 1f;

		return value;
	}

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

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

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

	private static void AddAssetPathCandidates( List<string> files, Editor.Asset asset )
	{
		if ( asset == null || asset.IsDeleted )
			return;

		if ( !string.IsNullOrWhiteSpace( asset.AbsolutePath ) )
			files.Add( asset.AbsolutePath );

		if ( !string.IsNullOrWhiteSpace( asset.RelativePath ) )
			files.Add( asset.RelativePath );

		if ( !string.IsNullOrWhiteSpace( asset.Path ) )
			files.Add( asset.Path );
	}
}