Editor/FenceLibrary/FenceDefinitionAssetContextMenu.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace Editor;

internal static class FenceDefinitionAssetContextMenu
{
	private const string FenceDefinitionExtension = ".fencedef";

	private static readonly string[] SupportedSourceExtensions =
	[
		".vmdl",
		".prefab"
	];

	[Event( "asset.contextmenu", Priority = 50 )]
	public static void OnAssetContextMenu( AssetContextMenu e )
	{
		if ( e.SelectedList is null || e.SelectedList.Count <= 0 )
			return;

		if ( !e.SelectedList.All( IsFenceSourceAsset ) )
			return;

		var sources = e.SelectedList
			.Select( FenceSource.FromAssetEntry )
			.Where( x => x.IsValid )
			.ToList();

		if ( sources.Count <= 0 )
			return;

		e.Menu.AddOption( "Create Fence Definition From Selected", "fence", action: () => CreateFenceDefinitionFromSources( sources ) );
	}

	private static bool IsFenceSourceAsset( AssetEntry entry )
	{
		if ( entry is null )
			return false;

		var extension = Path.GetExtension( entry.AbsolutePath );
		return SupportedSourceExtensions.Any( x => string.Equals( x, extension, StringComparison.OrdinalIgnoreCase ) );
	}

	private static void CreateFenceDefinitionFromSources( IReadOnlyList<FenceSource> sources )
	{
		if ( sources is null || sources.Count <= 0 )
			return;

		var firstSource = sources[0];
		var defaultDirectory = GetDefaultOutputDirectory( firstSource.AbsolutePath );
		var defaultFileName = $"{BuildDefinitionAssetName( sources )}{FenceDefinitionExtension}";

		var dialog = new FileDialog( null )
		{
			Title = "Create Fence Definition From Selected..",
			Directory = defaultDirectory,
			DefaultSuffix = FenceDefinitionExtension
		};

		dialog.SelectFile( defaultFileName );
		dialog.SetFindFile();
		dialog.SetModeSave();
		dialog.SetNameFilter( "Fence Definition (*.fencedef)" );

		if ( !dialog.Execute() )
			return;

		var selectedFile = EnsureExtension( dialog.SelectedFile, FenceDefinitionExtension );
		var outputDirectory = Path.GetDirectoryName( selectedFile );
		if ( string.IsNullOrWhiteSpace( outputDirectory ) )
			return;

		if ( !IsInsideAssetsDirectory( outputDirectory ) )
		{
			EditorUtility.DisplayDialog( "Invalid Fence Definition Folder", "Fence definitions must be saved inside this project's Assets folder." );
			return;
		}

		Directory.CreateDirectory( outputDirectory );

		if ( File.Exists( selectedFile ) )
		{
			EditorUtility.DisplayDialog( "Fence Definition Exists", "A fence definition already exists at that path. Choose a new file name." );
			return;
		}

		File.WriteAllText( selectedFile, BuildFenceDefinitionJson( sources ) );
		RegisterAndCompileFile( selectedFile );

		MainAssetBrowser.Instance?.Local.UpdateAssetList();

		EditorUtility.DisplayDialog( "Fence Definition Created", $"Created a fence definition with {sources.Count} selected source asset(s)." );
	}

	private static string BuildFenceDefinitionJson( IReadOnlyList<FenceSource> sources )
	{
		var entries = new JsonArray();

		foreach ( var source in sources )
		{
			entries.Add( new JsonObject
			{
				["DisplayName"] = string.Empty,
				["Model"] = source.IsModel ? source.ResourcePath : null,
				["Prefab"] = source.IsPrefab ? source.ResourcePath : null,
				["RotationOffset"] = "0,0,0",
				["Weight"] = 1
			} );
		}

		var root = new JsonObject
		{
			["RootName"] = "Fence",
			["Entries"] = entries,
			["UseWeightedRandom"] = true,
			["MaximumSegments"] = 256,
			["SegmentGap"] = 0,
			["RotationRandomizationMin"] = "0,0,0",
			["RotationRandomizationMax"] = "0,0,0",
			["DistanceRandomizationMin"] = 0,
			["DistanceRandomizationMax"] = 0,
			["CreateBlockerVolumes"] = true,
			["__references"] = new JsonArray(),
			["__version"] = 0
		};

		return root.ToJsonString( new JsonSerializerOptions { WriteIndented = true } );
	}

	private static void RegisterAndCompileFile( string absolutePath )
	{
		AssetSystem.RegisterFile( absolutePath );

		var resourcePath = GetProjectResourcePath( absolutePath );
		var asset = AssetSystem.FindByPath( resourcePath ) ?? AssetSystem.FindByPath( absolutePath );
		asset?.Compile( true );
	}

	private static string GetDefaultOutputDirectory( string sourceAbsolutePath )
	{
		var sourceDirectory = Path.GetDirectoryName( sourceAbsolutePath );
		if ( !string.IsNullOrWhiteSpace( sourceDirectory ) && IsInsideAssetsDirectory( sourceDirectory ) )
			return sourceDirectory;

		return Project.Current.GetAssetsPath();
	}

	private static string BuildDefinitionAssetName( IReadOnlyList<FenceSource> sources )
	{
		if ( sources.Count == 1 )
			return SanitizeFileName( sources[0].Name );

		var firstDirectory = Path.GetDirectoryName( sources[0].AbsolutePath );
		if ( !string.IsNullOrWhiteSpace( firstDirectory ) )
		{
			var directoryName = Path.GetFileName( firstDirectory );
			if ( !string.IsNullOrWhiteSpace( directoryName ) )
				return SanitizeFileName( $"{directoryName} Fence" );
		}

		return "Fence Definition";
	}

	private static bool IsInsideAssetsDirectory( string path )
	{
		var assetsPath = NormalizeDirectory( Project.Current.GetAssetsPath() );
		var targetPath = NormalizeDirectory( path );
		return targetPath.Equals( assetsPath, StringComparison.OrdinalIgnoreCase )
			|| targetPath.StartsWith( assetsPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase );
	}

	private static string GetProjectResourcePath( string absoluteSourcePath )
	{
		var absolutePath = Path.GetFullPath( absoluteSourcePath );
		var assetsPath = Path.GetFullPath( Project.Current.GetAssetsPath() );

		if ( absolutePath.StartsWith( NormalizeDirectory( assetsPath ) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase ) )
		{
			return Path.GetRelativePath( assetsPath, absolutePath ).Replace( '\\', '/' );
		}

		return Path.GetFileName( absolutePath ).Replace( '\\', '/' );
	}

	private static string SanitizeFileName( string name )
	{
		var invalid = Path.GetInvalidFileNameChars();
		var sanitized = new string( name.Select( c => invalid.Contains( c ) ? '_' : c ).ToArray() ).Trim();
		return string.IsNullOrWhiteSpace( sanitized ) ? "Fence Definition" : sanitized;
	}

	private static string EnsureExtension( string path, string extension )
	{
		return string.Equals( Path.GetExtension( path ), extension, StringComparison.OrdinalIgnoreCase )
			? path
			: Path.ChangeExtension( path, extension );
	}

	private static string NormalizeDirectory( string path )
	{
		return Path.GetFullPath( path ).TrimEnd( Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar );
	}

	private readonly record struct FenceSource( string AbsolutePath, string ResourcePath, string Name, string Extension )
	{
		public bool IsValid => !string.IsNullOrWhiteSpace( AbsolutePath ) && !string.IsNullOrWhiteSpace( ResourcePath );
		public bool IsModel => string.Equals( Extension, ".vmdl", StringComparison.OrdinalIgnoreCase );
		public bool IsPrefab => string.Equals( Extension, ".prefab", StringComparison.OrdinalIgnoreCase );

		public static FenceSource FromAssetEntry( AssetEntry entry )
		{
			if ( entry is null )
				return default;

			var absolutePath = Path.GetFullPath( entry.AbsolutePath );
			var resourcePath = entry.Asset?.RelativePath;
			if ( string.IsNullOrWhiteSpace( resourcePath ) )
			{
				resourcePath = GetProjectResourcePath( absolutePath );
			}

			return new FenceSource(
				absolutePath,
				resourcePath.Replace( '\\', '/' ),
				Path.GetFileNameWithoutExtension( absolutePath ),
				Path.GetExtension( absolutePath ) );
		}
	}
}