Code/FxboxParticleSystem.cs
using System;
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace fxbox;

/// <summary>
/// Component that integrates FXBox particle systems with s&box native particle components
/// </summary>
public class FXBoxNativeParticleSystem : Component, Component.ExecuteInEditor, ResourceLibrary.IEventListener
{
    [Property] public ParticleResource ParticleSystem { get; set; }
    
    [Property, Hide] private List<GameObject> Emitters { get; set; } = new();
    
    [Property] public bool PlayOnStart { get; set; } = true;
    [Property] public bool DestroyOnEnd { get; set; }

    public Vector3 Velocity;
    
    /// <summary>
    /// Instance-specific float parameter overrides
    /// </summary>
    [Property, Group("Parameters"), Title("Float Parameters")]
    public Dictionary<string, float> ParameterOverrides { get; set; } = new();
    
    /// <summary>
    /// Instance-specific vector parameter overrides
    /// </summary>
    [Property, Group("Parameters"), Title("Vector Parameters")]
    public Dictionary<string, Vector3> VectorParameterOverrides { get; set; } = new();
    
    /// <summary>
    /// Instance-specific color parameter overrides
    /// </summary>
    [Property, Group("Parameters"), Title("Color Parameters")]
    public Dictionary<string, ParticleGradient> ColorParameterOverrides { get; set; } = new();
    private int _version = 0;

    private Vector3 _lastPosition;

    protected override void OnFixedUpdate()
    {
	    Velocity = (WorldPosition - _lastPosition);
	    _lastPosition = WorldPosition;
        if (ParticleSystem != null && ParticleSystem.Version != _version)
        {
            _version = ParticleSystem.Version;
            UpdateEmitters();
        }
    }
    
    // ==================== FLOAT PARAMETERS ====================
    
    /// <summary>
    /// Set a float parameter value for this specific instance
    /// </summary>
    public void Set(string name, float value)
    {
        if (ParticleSystem == null) return;
        
        var param = ParticleSystem.FloatParameters?.FirstOrDefault(p => p.Name == name);
        if (param == null)
        {
            //Log.Warning($"Float parameter '{name}' not found in particle system");
            return;
        }
        
        ParameterOverrides[name] = value;
        UpdateParameterValues();
    }

    public void Burst()
    {
	    foreach ( var emitter in Emitters )
	    {
		    foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>(  ) )
		    {
			    var target = particleEmitter.GetComponent<ParticleEffect>();
			    particleEmitter.ResetEmitter();
		    }
	    }
    }

    public void Burst( Vector3 worldPosition )
    {
	    foreach ( var emitter in Emitters )
	    {
		    foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>(  ) )
		    {
			    var target = particleEmitter.GetComponent<ParticleEffect>();
			    for(int i=0; i< particleEmitter.Burst.Evaluate( Time.Delta, Time.Delta ); i++)
			    {
				    particleEmitter.Emit( target );
			    }
		    }
	    }
	    
	    foreach ( var emitter in Emitters )
	    {
		    foreach ( var particleEffect in emitter.GetComponentsInChildren<Sandbox.ParticleEffect>(  ) )
		    {
			    foreach ( var particle in particleEffect.Particles )
			    {
				    if ( particle.Age <= 0.01f )
				    {
						particle.Position += worldPosition - particleEffect.WorldTransform.Position;
				    }

			    }
		    }
	    }
    }
    
    /// <summary>
    /// Get a float parameter value (override or default)
    /// </summary>
    public float GetFloatParameter(string name)
    {
        if (ParameterOverrides.TryGetValue(name, out float overrideValue))
        {
            return overrideValue;
        }
        
        return ParticleSystem?.GetParameterDefault(name) ?? 0f;
    }
    
    /// <summary>
    /// Reset a float parameter to its default value
    /// </summary>
    public void ResetParameter(string name)
    {
        ParameterOverrides.Remove(name);
        UpdateParameterValues();
    }
    
    // ==================== VECTOR PARAMETERS ====================
    
    /// <summary>
    /// Set a vector parameter value for this specific instance
    /// </summary>
    public void Set(string name, Vector3 value)
    {
        if (ParticleSystem == null) return;
        
        var param = ParticleSystem.VectorParameters?.FirstOrDefault(p => p.Name == name);
        if (param == null)
        {
            //Log.Warning($"Vector parameter '{name}' not found in particle system");
            return;
        }
        
        VectorParameterOverrides[name] = value;
        UpdateParameterValues();
    }
    
    public int GetAliveParticles()
    {
	    var particles = 0;
	    foreach ( var emitter in Emitters )
	    {
		    var target = emitter.GetComponent<ParticleEffect>();
		    particles += target.ParticleCount;
	    }

	    return particles;
    }
    /// <summary>
    /// Get a vector parameter value (override or default)
    /// </summary>
    public Vector3 GetVectorParameter(string name)
    {
        if (VectorParameterOverrides.TryGetValue(name, out Vector3 overrideValue))
        {
            return overrideValue;
        }
        
        return ParticleSystem?.GetVectorParameterDefault(name) ?? Vector3.Zero;
    }
    
    /// <summary>
    /// Reset a vector parameter to its default value
    /// </summary>
    public void ResetVectorParameter(string name)
    {
        VectorParameterOverrides.Remove(name);
        UpdateParameterValues();
    }
    
    // ==================== COLOR PARAMETERS ====================
    
    /// <summary>
    /// Set a color parameter value for this specific instance
    /// </summary>
    public void Set(string name, Color value)
    {
        if (ParticleSystem == null) return;
        
        var param = ParticleSystem.ColorParameters?.FirstOrDefault(p => p.Name == name);
        if (param == null)
        {
            //Log.Warning($"Color parameter '{name}' not found in particle system");
            return;
        }
        
        ColorParameterOverrides[name] = value;
        UpdateParameterValues();
    }
    
    /// <summary>
    /// Get a color parameter value (override or default)
    /// </summary>
    public ParticleGradient GetColorParameter(string name)
    {
        if (ColorParameterOverrides.TryGetValue(name, out ParticleGradient overrideValue))
        {
            return overrideValue;
        }
        
        return ParticleSystem?.GetColorParameterDefault(name) ?? Color.White;
    }
    
    /// <summary>
    /// Reset a color parameter to its default value
    /// </summary>
    public void ResetColorParameter(string name)
    {
        ColorParameterOverrides.Remove(name);
        UpdateParameterValues();
    }
    
    // ==================== GENERAL ====================
    
    /// <summary>
    /// Reset all parameters to their default values
    /// </summary>
    public void ResetAllParameters()
    {
        ParameterOverrides.Clear();
        VectorParameterOverrides.Clear();
        ColorParameterOverrides.Clear();
        UpdateParameterValues();
    }
    
    /// <summary>
    /// Helper buttons for the inspector
    /// </summary>
    [Button("Reset All Parameters")]
    [Group("Parameters")]
    public void ResetAllParametersButton()
    {
        ResetAllParameters();
    }
    
    [Button("Initialize All Parameters")]
    [Group("Parameters")]
    [Description("Copy all parameters from the resource as overrides")]
    public void InitializeParametersFromResource()
    {
        if (ParticleSystem == null) return;
        
        ParameterOverrides.Clear();
        if (ParticleSystem.FloatParameters != null)
        {
            foreach (var param in ParticleSystem.FloatParameters)
            {
                ParameterOverrides[param.Name] = param.DefaultValue;
            }
        }
        
        VectorParameterOverrides.Clear();
        if (ParticleSystem.VectorParameters != null)
        {
            foreach (var param in ParticleSystem.VectorParameters)
            {
                VectorParameterOverrides[param.Name] = param.DefaultValue;
            }
        }
        
        ColorParameterOverrides.Clear();
        if (ParticleSystem.ColorParameters != null)
        {
            foreach (var param in ParticleSystem.ColorParameters)
            {
                ColorParameterOverrides[param.Name] = param.DefaultValue;
            }
        }
    }
    
    /// <summary>
    /// Update parameter values without rebuilding emitters
    /// </summary>
    private void UpdateParameterValues()
    {
        if (ParticleSystem?.Emitters == null) return;
        
        foreach (var emitterObject in Emitters)
        {
            if (!emitterObject.IsValid()) continue;
            
            var controller = emitterObject.GetComponent<FXBoxParticleController>();
            if (controller != null)
            {
                UpdateModulesWithParameters(emitterObject, controller.EmitterData);
            }
        }
    }
    
    public void OnSave(GameResource resource)
    {
        Log.Info("The scene has stopped");
    }
    
    protected override void OnStart()
    {
        UpdateEmitters();
    }

    protected override void OnEnabled()
    {
        UpdateEmitters();
        base.OnEnabled();
    }

    protected override void DrawGizmos()
    {
	    Gizmo.Hitbox.Sprite( 0, 50, false );
	    if ( Gizmo.IsHovered  || Gizmo.IsSelected)
	    {
		    Gizmo.Draw.Color = Color.White;
		    if ( Gizmo.IsSelected )
		    {
			    Gizmo.Draw.Color = Color.Yellow;
		    }
	    }
	    else
	    {
		    Gizmo.Draw.Color = Color.Gray;
	    }
	   
	    Gizmo.Draw.Sprite( 0, 50, Texture.Load( "images/particlehover.vtex" ), false );  
    }

    public void UpdateEmitters()
    {
        // Clean up existing emitters
        foreach (var emitter in Emitters)
        {
            emitter?.DestroyImmediate();
        }
        Emitters.Clear();

        if (ParticleSystem?.Emitters == null) return;

        // Create emitters from ParticleResource
        foreach (var emitterData in ParticleSystem.Emitters)
        {
            if (!emitterData.Enabled) continue;

            var emitterObject = new GameObject(GameObject);
            emitterObject.Flags = emitterObject.Flags.WithFlag( GameObjectFlags.Hidden, true );
            emitterObject.Name = emitterData.Name;
            Emitters.Add(emitterObject);

            // Add ParticleEffect component
            var particleEffect = emitterObject.GetOrAddComponent<ParticleEffect>();
            particleEffect.MaxParticles = emitterData.MaxParticles;

            // Create native components from modules
            CreateModuleComponents(emitterObject, emitterData);

            // Add controller to handle particle updates
            var controller = emitterObject.GetOrAddComponent<FXBoxParticleController>();
            controller.EmitterData = emitterData;
            controller.ParticleEffect = particleEffect;
            controller.InitializeModules = emitterData.InitializeModules;
            controller.ParticleSystemComponent = this;
            
        }
    }

    private void CreateModuleComponents(GameObject go, ParticleEmitter emitterData)
    {
        // Create components from all modules that implement IParticleComponentCreator
        var allModules = emitterData.SpawnModules
            .Concat(emitterData.InitializeModules)
            .Concat(emitterData.UpdateModules)
            .Concat(emitterData.RenderModules);

        var particleModules = allModules.ToList();
        foreach (var module in particleModules.OfType<IParticleComponentCreator>())
        {
            if (module is ParticleModule pm && pm.Enabled)
            {
                module.CreateComponent(go);
            }
        }

        var context = new ParticleExecutionContext();
        context.Effect = go.GetComponent<ParticleEffect>();
        context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();
        context.SystemComponent = this;

        foreach (var module in particleModules)
        {
            module.Initialize(context);
        }

        // Ensure we have at least a basic emitter if none was created
        if (!go.GetComponent<Sandbox.ParticleEmitter>().IsValid())
        {
            var emitter = go.AddComponent<ParticleSphereEmitter>();
            emitter.Duration = ParticleSystem.Duration;
            emitter.Loop = ParticleSystem.Looping;
            emitter.DestroyOnEnd = DestroyOnEnd;
        }

        var emitters = go.GetComponentsInChildren<Sandbox.ParticleEmitter>();
        foreach ( var emit in emitters )
        {
	        emit.Duration = ParticleSystem.Duration;
	        emit.Loop = ParticleSystem.Looping;
	        emit.DestroyOnEnd = DestroyOnEnd;
        }
    }
    
    private void UpdateModulesWithParameters(GameObject go, ParticleEmitter emitterData)
    {
        var context = new ParticleExecutionContext();
        context.Effect = go.GetComponent<ParticleEffect>();
        context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();
        context.SystemComponent = this;

        var allModules = emitterData.SpawnModules
            .Concat(emitterData.InitializeModules)
            .Concat(emitterData.UpdateModules)
            .Concat(emitterData.RenderModules);

        foreach (var module in allModules)
        {
            module.Initialize(context);
        }
    }
    
    protected override void OnDisabled()
    {
	    // Clean up existing emitters
	    foreach (var emitter in Emitters)
	    {
		    emitter?.Destroy();
	    }
	    
	    Emitters.Clear();
	    base.OnDisabled();
    }
}

/// <summary>
/// Controller that executes particle update modules on each particle
/// </summary>
public class FXBoxParticleController : ParticleController
{
    [Property, Hide] public ParticleEmitter EmitterData { get; set; }
    [Property, Hide] public new ParticleEffect ParticleEffect { get; set; }
    [Property, Hide] public FXBoxNativeParticleSystem ParticleSystemComponent { get; set; }
    private TimeSince _timeSinceCreated = 0;
    public List<ParticleModule> InitializeModules { get; set; } = new List<ParticleModule>();

    protected override void OnUpdate()
    {
	    if(_timeSinceCreated > ParticleSystemComponent.ParticleSystem.Duration && !ParticleSystemComponent.ParticleSystem.Looping&& ParticleEffect.Particles.Count <= 0)
	    {
		    if ( !Scene.IsEditor )
		    {
			    GameObject.Root.Destroy();
			    return; 
		    }
		    
	    }
	    var context = new ParticleExecutionContext
	    {
		    Particle = null,
		    Effect = ParticleEffect,
		    Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
		    SystemComponent = ParticleSystemComponent
	    };
	    foreach ( var init in EmitterData.InitializeModules )
	    {
		    init.Execute( context );
	    }
	    foreach ( var spawn in EmitterData.SpawnModules )
	    {
		    spawn.Execute( context );
	    }
    }

    protected override void OnParticleStep(Particle particle, float delta)
    {
        base.OnParticleStep(particle, delta);

        if (EmitterData == null) return;
        
        var context = new ParticleExecutionContext
        {
            Particle = particle,
            Effect = ParticleEffect,
            Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
            SystemComponent = ParticleSystemComponent
        };
        
        // Execute all update modules that implement IParticleUpdater
        foreach (var module in EmitterData.UpdateModules.OfType<IParticleUpdater>())
        {
            if (module is ParticleModule pm && pm.Enabled)
            {
                module.UpdateParticle(context, delta);
            }
        }
    }
    
    protected override void OnParticleCreated(Particle p)
    {
        p.Position = ParticleEffect.WorldTransform.Position;
        InitializeModules ??= EmitterData?.InitializeModules ?? new List<ParticleModule>();
        
        var context = new ParticleExecutionContext
        {
            Particle = p,
            Effect = ParticleEffect,
            Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
            SystemComponent = ParticleSystemComponent
        };
        
        foreach (var module in InitializeModules)
        {
            module.Initialize(context);
        }
    }
}

/// <summary>
/// Interface for modules that can create native components
/// </summary>
public interface IParticleComponentCreator
{
    void CreateComponent(GameObject go);
}

/// <summary>
/// Interface for modules that update particles
/// </summary>
public interface IParticleUpdater
{
    void UpdateParticle(ParticleExecutionContext particle, float delta);
}

[Flags]
public enum FXCopyFlags
{
	Rotation = 1,
	Scale = 2,
}