Editor/LightingToolHandlers.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Editor;
using Sandbox;
using InsideGeometryBehaviorType = Sandbox.IndirectLightVolume.InsideGeometryBehavior;

namespace SboxMcpServer;

/// <summary>
/// Lighting MCP tools: create_light, configure_light, create_sky_box, set_sky_box, create_ambient_light.
/// </summary>
internal static class LightingToolHandlers
{
	private static readonly JsonSerializerOptions _json = new()
	{
		PropertyNamingPolicy   = JsonNamingPolicy.CamelCase,
		DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
	};

	// ── create_light ──────────────────────────────────────────────────────

	internal static object CreateLight( JsonElement args )
	{
		var scene = OzmiumSceneHelpers.ResolveScene();
		if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );

		string type = OzmiumSceneHelpers.Get( args, "type", "PointLight" );
		float  x    = OzmiumSceneHelpers.Get( args, "x", 0f );
		float  y    = OzmiumSceneHelpers.Get( args, "y", 0f );
		float  z    = OzmiumSceneHelpers.Get( args, "z", 0f );
		float  pitch = OzmiumSceneHelpers.Get( args, "pitch", 0f );
		float  yaw   = OzmiumSceneHelpers.Get( args, "yaw", 0f );
		float  roll  = OzmiumSceneHelpers.Get( args, "roll", 0f );
		string name  = OzmiumSceneHelpers.Get( args, "name", (string)null );

		try
		{
			var go = scene.CreateObject();
			go.WorldPosition = new Vector3( x, y, z );
			go.WorldRotation = Rotation.From( pitch, yaw, roll );

			Component light;
			switch ( type.ToLowerInvariant() )
				{
				case "pointlight":
				case "point":
					light = go.Components.Create<PointLight>();
					go.Name = name ?? "Point Light";
					break;
				case "spotlight":
				case "spot":
					light = go.Components.Create<SpotLight>();
					go.Name = name ?? "Spot Light";
					break;
				case "directionallight":
				case "directional":
					light = go.Components.Create<DirectionalLight>();
					go.Name = name ?? "Directional Light";
					break;
				default:
					return OzmiumSceneHelpers.Txt( $"Unknown light type '{type}'. Use: PointLight, SpotLight, DirectionalLight." );
			}

			// Apply optional light color
			if ( args.TryGetProperty( "color", out var colEl ) && colEl.ValueKind == JsonValueKind.String )
			{
				var colorStr = colEl.GetString();
				if ( !string.IsNullOrEmpty( colorStr ) )
				{
					try
					{
						var color = Color.Parse( colorStr ) ?? default;
						var prop = light.GetType().GetProperty( "LightColor" );
						prop?.SetValue( light, color );
					}
					catch { }
				}
			}

			// Apply optional shadows
			if ( args.TryGetProperty( "shadows", out var shEl ) && shEl.ValueKind == JsonValueKind.False )
			{
				var prop = light.GetType().GetProperty( "Shadows" );
				prop?.SetValue( light, false );
			}

			// Set radius and attenuation for point/spot lights
			if ( light is PointLight pl )
			{
				pl.Radius = OzmiumSceneHelpers.Get( args, "radius", pl.Radius );
				pl.Attenuation = OzmiumSceneHelpers.Get( args, "attenuation", pl.Attenuation );
			}
			else if ( light is SpotLight sl )
			{
				sl.Radius = OzmiumSceneHelpers.Get( args, "radius", sl.Radius );
				sl.Attenuation = OzmiumSceneHelpers.Get( args, "attenuation", sl.Attenuation );
				sl.ConeOuter = OzmiumSceneHelpers.Get( args, "coneOuter", sl.ConeOuter );
				sl.ConeInner = OzmiumSceneHelpers.Get( args, "coneInner", sl.ConeInner );
			}

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				message = $"Created {type} '{go.Name}'.",
				id       = go.Id.ToString(),
				position = OzmiumSceneHelpers.V3( go.WorldPosition )
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── configure_light ────────────────────────────────────────────────────

	internal static object ConfigureLight( JsonElement args )
	{
		var scene = OzmiumSceneHelpers.ResolveScene();
		if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );

		string id   = OzmiumSceneHelpers.Get( args, "id",   (string)null );
		string name = OzmiumSceneHelpers.Get( args, "name", (string)null );

		var go = OzmiumSceneHelpers.FindGo( scene, id, name );
		if ( go == null ) return OzmiumSceneHelpers.Txt( "Object not found." );

		var light = go.Components.GetAll().FirstOrDefault( c => c is Light ) as Light;
		if ( light == null ) return OzmiumSceneHelpers.Txt( $"No Light component found on '{go.Name}'." );

		try
		{
			if ( args.TryGetProperty( "color", out var colEl ) && colEl.ValueKind == JsonValueKind.String )
			{
				var c = Color.Parse( colEl.GetString() ) ?? default;
				var p = light.GetType().GetProperty( "LightColor" );
				p?.SetValue( light, c );
			}

			if ( args.TryGetProperty( "shadows", out var shEl ) )
			{
				var p = light.GetType().GetProperty( "Shadows" );
				p?.SetValue( light, shEl.GetBoolean() );
			}

			if ( args.TryGetProperty( "radius", out var radEl ) )
			{
				var p = light.GetType().GetProperty( "Radius" );
				if ( p != null ) p.SetValue( light, radEl.GetSingle() );
			}

			if ( args.TryGetProperty( "attenuation", out var attEl ) )
			{
				var p = light.GetType().GetProperty( "Attenuation" );
				if ( p != null ) p.SetValue( light, attEl.GetSingle() );
			}

			if ( args.TryGetProperty( "coneOuter", out var coEl ) )
				{
				var p = light.GetType().GetProperty( "ConeOuter" );
				if ( p != null ) p.SetValue( light, coEl.GetSingle() );
			}

			if ( args.TryGetProperty( "coneInner", out var ciEl ) )
			{
				var p = light.GetType().GetProperty( "ConeInner" );
				if ( p != null ) p.SetValue( light, ciEl.GetSingle() );
			}

			if ( args.TryGetProperty( "fogMode", out var fmEl ) && fmEl.ValueKind == JsonValueKind.String )
			{
				var p = light.GetType().GetProperty( "FogMode" );
				if ( p != null )
				{
					var val = Enum.Parse( p.PropertyType, fmEl.GetString(), ignoreCase: true );
					p.SetValue( light, val );
				}
			}

			if ( args.TryGetProperty( "fogStrength", out var fsEl ) )
			{
				var p = light.GetType().GetProperty( "FogStrength" );
				if ( p != null ) p.SetValue( light, fsEl.GetSingle() );
			}

			return OzmiumSceneHelpers.Txt( $"Configured light on '{go.Name}' ({light.GetType().Name})." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── create_sky_box ──────────────────────────────────────────────────────

	internal static object CreateSkyBox( JsonElement args )
	{
		var scene = OzmiumSceneHelpers.ResolveScene();
		if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );

		float  x = OzmiumSceneHelpers.Get( args, "x", 0f );
		float  y = OzmiumSceneHelpers.Get( args, "y", 0f );
		float  z = OzmiumSceneHelpers.Get( args, "z", 0f );
		string name = OzmiumSceneHelpers.Get( args, "name", "Sky Box" );
		string skyMaterial = OzmiumSceneHelpers.Get( args, "skyMaterial", "materials/skybox/skybox_day_01.vmat" );

		try
		{
			var go = scene.CreateObject();
			go.Name = name;
			go.WorldPosition = new Vector3( x, y, z );

			var sky = go.Components.Create<SkyBox2D>();

			if ( !string.IsNullOrEmpty( skyMaterial ) )
				{
				var mat = Material.Load( skyMaterial );
				if ( mat != null ) sky.SkyMaterial = mat;
			}

			if ( args.TryGetProperty( "tint", out var tintEl ) && tintEl.ValueKind == JsonValueKind.String )
			{
				try { sky.Tint = Color.Parse( tintEl.GetString() ) ?? default; } catch { }
			}

			if ( args.TryGetProperty( "skyIndirectLighting", out var iblEl ) )
			{
				sky.SkyIndirectLighting = iblEl.GetBoolean();
			}

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				message = $"Created SkyBox '{go.Name}'.",
				id       = go.Id.ToString(),
				position = OzmiumSceneHelpers.V3( go.WorldPosition )
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── set_sky_box ──────────────────────────────────────────────────────────

	internal static object SetSkyBox( JsonElement args )
	{
		var scene = OzmiumSceneHelpers.ResolveScene();
		if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );

		string id   = OzmiumSceneHelpers.Get( args, "id",   (string)null );
		string name = OzmiumSceneHelpers.Get( args, "name", (string)null );

		var go = OzmiumSceneHelpers.FindGo( scene, id, name );
		if ( go == null ) return OzmiumSceneHelpers.Txt( "Object not found." );

		var sky = go.Components.GetAll().FirstOrDefault( c => c is SkyBox2D ) as SkyBox2D;
		if ( sky == null ) return OzmiumSceneHelpers.Txt( $"No SkyBox2D component found on '{go.Name}'." );

		try
		{
			if ( args.TryGetProperty( "skyMaterial", out var matEl ) && matEl.ValueKind == JsonValueKind.String )
			{
				var mat = Material.Load( matEl.GetString() );
				if ( mat != null ) ((SkyBox2D)sky).SkyMaterial = mat;
			}

			if ( args.TryGetProperty( "tint", out var tintEl ) && tintEl.ValueKind == JsonValueKind.String )
			{
				try { ((SkyBox2D)sky).Tint = Color.Parse( tintEl.GetString() ) ?? default; } catch { }
			}

			if ( args.TryGetProperty( "skyIndirectLighting", out var iblEl ) )
			{
				((SkyBox2D)sky).SkyIndirectLighting = iblEl.GetBoolean();
			}

			return OzmiumSceneHelpers.Txt( $"Updated SkyBox on '{go.Name}'." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── create_ambient_light ─────────────────────────────────────────────────

	internal static object CreateAmbientLight( JsonElement args )
	{
		var scene = OzmiumSceneHelpers.ResolveScene();
		if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );

		float  x    = OzmiumSceneHelpers.Get( args, "x", 0f );
		float  y    = OzmiumSceneHelpers.Get( args, "y", 0f );
		float  z    = OzmiumSceneHelpers.Get( args, "z", 0f );
		string name = OzmiumSceneHelpers.Get( args, "name", "Ambient Light" );
		string color = OzmiumSceneHelpers.Get( args, "color", "Gray" );

		try
		{
			var go = scene.CreateObject();
			go.Name = name;
			go.WorldPosition = new Vector3( x, y, z );

			var amb = go.Components.Create<AmbientLight>();
			try { amb.Color = Color.Parse( color ) ?? default; } catch { }

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				message = $"Created AmbientLight '{go.Name}'.",
				id       = go.Id.ToString(),
				position = OzmiumSceneHelpers.V3( go.WorldPosition )
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── Schemas ─────────────────────────────────────────────────────────────

	internal static Dictionary<string, object> Schema( string name, string desc, Dictionary<string, object> props, string[] req = null )
	{
		var schema = new Dictionary<string, object> { ["type"] = "object", ["properties"] = props };
		if ( req != null ) schema["required"] = req;
		return new Dictionary<string, object> { ["name"] = name, ["description"] = desc, ["inputSchema"] = schema };
	}

	internal static readonly Dictionary<string, object> LightTypes = new()
	{
		["type"] = "string",
		["description"] = "Type of light to create.",
		["enum"] = new[] { "PointLight", "SpotLight", "DirectionalLight" }
	};

	internal static readonly Dictionary<string, object> ColorProp = new()
	{
		["type"] = "string", ["description"] = "Light color hex string (e.g. '#FF8800')."
	};

	internal static readonly Dictionary<string, object> FogModes = new()
	{
		["type"] = "string",
		["description"] = "Fog influence mode.",
		["enum"] = new[] { "Disabled", "Enabled", "WithoutShadows" }
	};

	internal static Dictionary<string, object> SchemaCreateLight => Schema( "create_light",
		"Creates a GO with a light component (PointLight/SpotLight/DirectionalLight).",
		new Dictionary<string, object>
		{
			["type"]    = LightTypes,
			["x"]       = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
			["y"]       = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
			["z"]       = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
			["pitch"]   = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Pitch rotation in degrees." },
			["yaw"]     = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Yaw rotation in degrees." },
			["roll"]    = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Roll rotation in degrees." },
			["name"]    = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
			["color"]   = ColorProp,
			["shadows"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Cast shadows (default true)." },
			["radius"]      = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Light radius (PointLight/SpotLight)." },
			["attenuation"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Light attenuation (PointLight/SpotLight)." },
			["coneOuter"]    = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Outer cone angle (SpotLight)." },
			["coneInner"]    = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Inner cone angle (SpotLight)." }
		},
		new[] { "type" } );

	internal static Dictionary<string, object> SchemaConfigureLight => Schema( "configure_light",
		"Sets properties on an existing Light component on a GameObject.",
		new Dictionary<string, object>
		{
			["id"]            = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
			["name"]          = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." },
			["color"]         = ColorProp,
			["shadows"]       = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Cast shadows." },
			["radius"]        = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Light radius." },
			["attenuation"]   = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Light attenuation." },
			["coneOuter"]      = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Outer cone angle (SpotLight)." },
			["coneInner"]      = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Inner cone angle (SpotLight)." },
			["fogMode"]       = FogModes,
			["fogStrength"]   = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Fog strength (0-1)." }
		} );

	internal static Dictionary<string, object> SchemaCreateSkyBox => Schema( "create_sky_box",
		"Creates a GO with a SkyBox2D component for sky rendering.",
		new Dictionary<string, object>
		{
			["x"]                = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
			["y"]                = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
			["z"]                = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
			["name"]              = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
			["skyMaterial"]      = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Sky material path (e.g. 'materials/skybox/skybox_day_01.vmat')." },
			["tint"]             = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Tint color hex string." },
			["skyIndirectLighting"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Use sky for indirect lighting (default true)." }
		} );

	internal static Dictionary<string, object> SchemaSetSkyBox => Schema( "set_sky_box",
		"Configures an existing SkyBox2D component.",
		new Dictionary<string, object>
		{
			["id"]                = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
			["name"]              = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." },
			["skyMaterial"]       = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Sky material path." },
			["tint"]             = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Tint color hex string." },
			["skyIndirectLighting"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Use sky for indirect lighting." }
		} );

	internal static Dictionary<string, object> SchemaCreateAmbientLight => Schema( "create_ambient_light",
		"Creates/updates a scene-level AmbientLight for global ambient illumination.",
		new Dictionary<string, object>
		{
			["x"]     = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
			["y"]     = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
			["z"]     = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
			["name"]   = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
			["color"]  = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Ambient color (default 'Gray')." }
		} );

	// ── create_indirect_light_volume ──────────────────────────────────────

	internal static object CreateIndirectLightVolume( JsonElement args )
	{
		var scene = OzmiumSceneHelpers.ResolveScene();
		if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );

		float  x    = OzmiumSceneHelpers.Get( args, "x", 0f );
		float  y    = OzmiumSceneHelpers.Get( args, "y", 0f );
		float  z    = OzmiumSceneHelpers.Get( args, "z", 0f );
		string name = OzmiumSceneHelpers.Get( args, "name", "Indirect Light Volume" );

		try
		{
			var go = scene.CreateObject();
			go.Name = name;
			go.WorldPosition = new Vector3( x, y, z );

			var ilg = go.Components.Create<IndirectLightVolume>();

			if ( args.TryGetProperty( "size", out var szEl ) && szEl.ValueKind == JsonValueKind.Object )
			{
				ilg.Bounds = BBox.FromPositionAndSize( 0,
					new Vector3(
						OzmiumSceneHelpers.Get( szEl, "x", 512f ),
						OzmiumSceneHelpers.Get( szEl, "y", 512f ),
						OzmiumSceneHelpers.Get( szEl, "z", 512f ) ) );
			}

			ilg.ProbeDensity = OzmiumSceneHelpers.Get( args, "probeDensity", 8 );
			ilg.NormalBias = OzmiumSceneHelpers.Get( args, "normalBias", 5f );
			ilg.Contrast = OzmiumSceneHelpers.Get( args, "contrast", 1f );

			if ( args.TryGetProperty( "insideGeometryBehavior", out var igbEl ) && igbEl.ValueKind == JsonValueKind.String )
			{
				if ( Enum.TryParse<InsideGeometryBehaviorType>( igbEl.GetString(), true, out var igb ) )
				{
					// Use reflection to find the correct property name, as it changed from the enum name 'InsideGeometryBehavior'
					var behaviorProp = typeof( IndirectLightVolume ).GetProperties()
						.FirstOrDefault( p => p.PropertyType == typeof( InsideGeometryBehaviorType ) );
					behaviorProp?.SetValue( ilg, igb );
				}
			}

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				message = $"Created IndirectLightVolume '{go.Name}'.",
				id       = go.Id.ToString(),
				position = OzmiumSceneHelpers.V3( go.WorldPosition ),
				probeDensity = ilg.ProbeDensity
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	internal static Dictionary<string, object> SchemaCreateIndirectLightVolume => Schema( "create_indirect_light_volume",
		"Create a GO with an IndirectLightVolume (DDGI) for dynamic global illumination. Places a probe grid for real-time bounce light.",
		new Dictionary<string, object>
		{
			["x"]                       = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
			["y"]                       = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
			["z"]                       = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
			["name"]                     = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
			["size"]                     = new Dictionary<string, object> { ["type"] = "object", ["description"] = "Volume size {x,y,z} (default 512,512,512)." },
			["probeDensity"]             = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Probe density (default 8)." },
			["normalBias"]               = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Normal bias (default 5)." },
			["contrast"]                 = new Dictionary<string, object> { ["type"] = "number", ["description"] = "GI contrast (default 1)." },
			["insideGeometryBehavior"]   = new Dictionary<string, object>
			{
				["type"] = "string", ["description"] = "Behavior when probes are inside geometry.",
				["enum"] = new[] { "Deactivate", "Relocate" }
			}
		} );
}