Editor/UltimateLightManagerBuilderWindow.cs

Editor window for the Ultimate Light Manager builder. It renders UI to pick, save, delete and apply built-in or custom light presets, persist builder state to project settings, capture settings from selected scene objects, and create configured UltimateLightManager components on new GameObjects.

File AccessReflection
using Sandbox;
using global::Editor;
using Dreams.UltimateLightManager;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Dreams.UltimateLightManager.Editor;

internal sealed class UltimateLightManagerBuilderWindow : Window
{
    private const string StatePath = "ultimate_light_manager_builder.json";

    private static UltimateLightManagerBuilderWindow Instance;

    private readonly UltimateLightManagerBuilderState _state;
    private readonly UltimateLightManagerBuilderDraft _draft;
    private readonly SerializedObject _draftObject;
    private readonly List<PresetEntry> _presetEntries = new();

    private ComboBox _presetCombo;
    private bool _isUpdatingPresetCombo;

    private UltimateLightManagerBuilderWindow()
    {
        Parent = EditorWindow;
        WindowFlags = WindowFlags.Dialog | WindowFlags.Customized | WindowFlags.CloseButton | WindowFlags.WindowSystemMenuHint | WindowFlags.WindowTitle;
        DeleteOnClose = true;
        WindowTitle = "Ultimate Light Builder";
        Size = new Vector2( 520, 760 );
        MinimumSize = new Vector2( 430, 620 );
        StatusBar.Visible = false;
        SetWindowIcon( "tungsten" );

        _state = LoadState();
        _draft = _state.Draft ?? new UltimateLightManagerBuilderDraft();
        _state.Draft = _draft;

        _draftObject = _draft.GetSerialized();
        _draftObject.OnPropertyChanged += OnDraftPropertyChanged;

        BuildUi();
        Show();
    }

    protected override void OnClosed()
    {
        if ( ReferenceEquals( Instance, this ) )
        {
            Instance = null;
        }

        base.OnClosed();
    }

    private void BuildUi()
    {
        Canvas = new Widget( this );
        Canvas.OnPaintOverride = () =>
        {
            Paint.ClearPen();
            Paint.SetBrush( Theme.WidgetBackground );
            Paint.DrawRect( Canvas.LocalRect );
            return true;
        };

        Canvas.Layout = Layout.Column();
        Canvas.Layout.Margin = 8;
        Canvas.Layout.Spacing = 8;

        var header = Canvas.Layout.AddColumn();
        header.Spacing = 4;

        header.Add( new Label( "Ultimate Light Builder", this ) )
            .SetStyles( "font-size: 18px; font-weight: bold;" );

        var subtitle = header.Add( new Label( "Creer rapidement une point light ou une spot light deja prete avec Ultimate Light Manager.", this ) );
        subtitle.WordWrap = true;
        subtitle.SetStyles( $"color: {Theme.Text.WithAlpha( 0.7f ).Hex};" );

        Canvas.Layout.AddSeparator();

        var presetSection = Canvas.Layout.AddColumn();
        presetSection.Spacing = 6;

        var presetRow = presetSection.AddRow();
        presetRow.Spacing = 6;
        presetRow.Add( new Label( "Charger preset", this ) { FixedWidth = 110, Alignment = TextFlag.LeftCenter } );

        _presetCombo = presetRow.Add( new ComboBox( this ), 1 );
        _presetCombo.ItemChanged += OnPresetSelectionChanged;
        _presetCombo.ToolTip = "Charge un preset builtin ou un preset custom.";

        presetRow.Add( new Button( "Depuis la selection", this )
        {
            Icon = "south_west",
            Clicked = LoadFromSelection
        } );

        var actionRow = presetSection.AddRow();
        actionRow.Spacing = 6;
        actionRow.Add( new Button( "Enregistrer preset", this )
        {
            Icon = "save",
            Clicked = OpenSavePresetDialog
        }, 1 );
        actionRow.Add( new Button( "Supprimer preset", this )
        {
            Icon = "delete",
            Clicked = ConfirmDeleteSelectedPreset
        }, 1 );
        actionRow.Add( new Button( "Reinitialiser", this )
        {
            Icon = "restart_alt",
            Clicked = ResetDraft
        }, 1 );

        RebuildPresetCombo();

        Canvas.Layout.AddSeparator();

        var scroll = Canvas.Layout.Add( new ScrollArea( this ), 1 );
        scroll.Canvas = new Widget( scroll );
        scroll.Canvas.Layout = Layout.Column();
        scroll.Canvas.Layout.Spacing = 6;
        scroll.Canvas.Layout.Margin = 0;

        var sheet = new ControlSheet();
        sheet.AddObject( _draftObject );
        scroll.Canvas.Layout.Add( sheet );
        scroll.Canvas.Layout.AddStretchCell();

        Canvas.Layout.AddSeparator();

        var footer = Canvas.Layout.AddColumn();
        footer.Spacing = 6;

        var help = footer.Add( new Label( "Les presets custom enregistrent les reglages de base du builder. Les presets builtin gardent leurs effets speciaux a la creation.", this ) );
        help.WordWrap = true;
        help.SetStyles( $"color: {Theme.Text.WithAlpha( 0.7f ).Hex};" );

        footer.Add( new Button.Primary( "Creer la light", "add_circle" )
        {
            Clicked = CreateLightFromDraft
        } );
    }

    private void OnDraftPropertyChanged( SerializedProperty property )
    {
        SaveState();
    }

    private void OnPresetSelectionChanged()
    {
        if ( _isUpdatingPresetCombo )
        {
            return;
        }

        int index = _presetCombo?.CurrentIndex ?? -1;
        if ( index < 0 || index >= _presetEntries.Count )
        {
            return;
        }

        var entry = _presetEntries[index];
        _state.SelectedPresetKey = entry.Key;

        if ( entry.IsCustom )
        {
            _draft.ApplyPresetData( entry.CustomPreset.Data );
        }
        else
        {
            ApplyBuiltInPresetToDraft( _draft, entry.BuiltInPreset );
        }

        SaveState();
        BuildUi();
    }

    private void OpenSavePresetDialog()
    {
        Dialog.AskString( SavePreset, "Nom du preset :", okay: "Enregistrer", title: "Enregistrer un preset", minLength: 1 );
    }

    private void SavePreset( string presetName )
    {
        presetName = presetName?.Trim();
        if ( string.IsNullOrWhiteSpace( presetName ) )
        {
            return;
        }

        var existing = _state.CustomPresets.FirstOrDefault( x => string.Equals( x.Name, presetName, StringComparison.OrdinalIgnoreCase ) );
        if ( existing == null )
        {
            existing = new UltimateLightManagerCustomPreset
            {
                Name = presetName
            };

            _state.CustomPresets.Add( existing );
        }

        existing.Name = presetName;
        existing.Data = _draft.ToPresetData();
        _state.SelectedPresetKey = GetCustomPresetKey( presetName );

        SaveState();
        BuildUi();
    }

    private void ConfirmDeleteSelectedPreset()
    {
        var selectedPreset = GetSelectedCustomPreset();
        if ( selectedPreset == null )
        {
            return;
        }

        Dialog.AskConfirm(
            () => DeletePreset( selectedPreset ),
            $"Supprimer le preset \"{selectedPreset.Name}\" ?",
            "Supprimer le preset",
            "Supprimer",
            "Annuler"
        );
    }

    private void DeletePreset( UltimateLightManagerCustomPreset preset )
    {
        _state.CustomPresets.Remove( preset );
        _state.SelectedPresetKey = GetBuiltInPresetKey( UltimateLightManager.LightPreset.Custom );
        SaveState();
        BuildUi();
    }

    private void ResetDraft()
    {
        string lightName = _draft.LightName;
        var lightType = _draft.TargetLightType;

        _draft.ApplyPresetData( new UltimateLightManagerPresetData() );
        _draft.LightName = lightName;
        _draft.TargetLightType = lightType;
        _state.SelectedPresetKey = GetBuiltInPresetKey( UltimateLightManager.LightPreset.Custom );

        SaveState();
        BuildUi();
    }

    private void LoadFromSelection()
    {
        var selectedLight = GetSelectedLightManager();
        if ( selectedLight == null )
        {
            return;
        }

        _draft.LightName = string.IsNullOrWhiteSpace( selectedLight.GameObject?.Name ) ? _draft.LightName : selectedLight.GameObject.Name;
        _draft.CaptureFromLight( selectedLight );

        _state.SelectedPresetKey = GetBuiltInPresetKey( selectedLight.SelectedPreset );

        SaveState();
        BuildUi();
    }

    private void CreateLightFromDraft()
    {
        var session = SceneEditorSession.Active;
        if ( session?.Scene == null )
        {
            return;
        }

        using var scope = SceneEditorSession.Scope();
        using ( session.UndoScope( $"Create {_draft.GetSafeLightName()}" ).WithGameObjectCreations().WithComponentCreations().Push() )
        {
            var go = new GameObject( true, _draft.GetSafeLightName() );
            var parent = GetSelectedParentGameObject();

            if ( parent != null )
            {
                go.WorldTransform = parent.WorldTransform;
                go.SetParent( parent, true );
            }

            var light = go.GetOrAddComponent<UltimateLightManager>( true );
            ApplyDraftToLight( _draft, light );

            EditorScene.Selection.Clear();
            EditorScene.Selection.Add( go );
        }

        SaveState();
    }

    private void RebuildPresetCombo()
    {
        if ( _presetCombo == null )
        {
            return;
        }

        _isUpdatingPresetCombo = true;
        _presetEntries.Clear();
        _presetCombo.Clear();

        AddBuiltInPreset( UltimateLightManager.LightPreset.Custom, "Builtin - Custom" );
        AddBuiltInPreset( UltimateLightManager.LightPreset.Candle, "Builtin - Candle" );
        AddBuiltInPreset( UltimateLightManager.LightPreset.Torch, "Builtin - Torch" );
        AddBuiltInPreset( UltimateLightManager.LightPreset.Neon, "Builtin - Neon" );
        AddBuiltInPreset( UltimateLightManager.LightPreset.Alarm, "Builtin - Alarm" );
        AddBuiltInPreset( UltimateLightManager.LightPreset.BrokenLamp, "Builtin - Broken Lamp" );
        AddBuiltInPreset( UltimateLightManager.LightPreset.SciFi, "Builtin - Sci-Fi" );
        AddBuiltInPreset( UltimateLightManager.LightPreset.StreetLight, "Builtin - Street Light" );

        foreach ( var preset in _state.CustomPresets
            .Where( x => !string.IsNullOrWhiteSpace( x?.Name ) )
            .OrderBy( x => x.Name, StringComparer.OrdinalIgnoreCase ) )
        {
            _presetEntries.Add( PresetEntry.ForCustom( preset ) );
            _presetCombo.AddItem( $"Custom - {preset.Name}" );
        }

        string selectedKey = _state.SelectedPresetKey;
        if ( string.IsNullOrWhiteSpace( selectedKey ) )
        {
            selectedKey = GetBuiltInPresetKey( UltimateLightManager.LightPreset.Custom );
            _state.SelectedPresetKey = selectedKey;
        }

        int selectedIndex = _presetEntries.FindIndex( x => x.Key == selectedKey );
        _presetCombo.CurrentIndex = selectedIndex >= 0 ? selectedIndex : 0;
        _isUpdatingPresetCombo = false;
    }

    private void AddBuiltInPreset( UltimateLightManager.LightPreset preset, string label )
    {
        _presetEntries.Add( PresetEntry.ForBuiltIn( preset ) );
        _presetCombo.AddItem( label );
    }

    private UltimateLightManagerCustomPreset GetSelectedCustomPreset()
    {
        int index = _presetCombo?.CurrentIndex ?? -1;
        if ( index < 0 || index >= _presetEntries.Count )
        {
            return null;
        }

        return _presetEntries[index].IsCustom ? _presetEntries[index].CustomPreset : null;
    }

    private static void ApplyBuiltInPresetToDraft( UltimateLightManagerBuilderDraft draft, UltimateLightManager.LightPreset preset )
    {
        draft.SelectedPreset = preset;
        draft.AutoApplyPreset = false;

        if ( preset == UltimateLightManager.LightPreset.Custom )
        {
            return;
        }

        ResetDraftLookSettings( draft );

        switch ( preset )
        {
            case UltimateLightManager.LightPreset.Candle:
                draft.Brightness = 0.75f;
                draft.LightColor = new Color( 1.0f, 0.76f, 0.5f );
                draft.SecondaryColor = new Color( 1.0f, 0.66f, 0.35f );
                draft.EnableKelvin = true;
                draft.KelvinTemperature = 1800;
                draft.EnableFire = true;
                draft.FireSpeed = 10.0f;
                draft.FireIntensity = 0.18f;
                draft.FireChaos = 0.6f;
                draft.VolumetricBoost = 0.6f;
                break;

            case UltimateLightManager.LightPreset.Torch:
                draft.Brightness = 1.35f;
                draft.LightColor = new Color( 1.0f, 0.72f, 0.38f );
                draft.SecondaryColor = new Color( 1.0f, 0.45f, 0.2f );
                draft.EnableKelvin = true;
                draft.KelvinTemperature = 2200;
                draft.EnableFire = true;
                draft.FireSpeed = 13.0f;
                draft.FireIntensity = 0.28f;
                draft.FireChaos = 1.0f;
                draft.VolumetricBoost = 1.4f;
                break;

            case UltimateLightManager.LightPreset.Neon:
                draft.Brightness = 1.15f;
                draft.LightColor = new Color( 0.2f, 0.95f, 1.0f );
                draft.SecondaryColor = new Color( 1.0f, 0.2f, 0.85f );
                draft.EnableColorTransition = true;
                draft.ColorTransitionSpeed = 0.65f;
                draft.EnablePulse = true;
                draft.PulseSpeed = 1.2f;
                draft.PulseMin = 0.75f;
                draft.CastShadows = false;
                draft.VolumetricBoost = 0.25f;
                break;

            case UltimateLightManager.LightPreset.Alarm:
                draft.Brightness = 2.0f;
                draft.LightColor = new Color( 1.0f, 0.18f, 0.12f );
                draft.AlarmColor = draft.LightColor;
                draft.EnableStrobe = true;
                draft.StrobeSpeed = 7.0f;
                draft.StrobeDutyCycle = 0.45f;
                draft.CastShadows = false;
                draft.VolumetricBoost = 1.1f;
                break;

            case UltimateLightManager.LightPreset.BrokenLamp:
                draft.Brightness = 1.0f;
                draft.LightColor = new Color( 1.0f, 0.93f, 0.82f );
                draft.EnableHorror = true;
                draft.MinFlickerDelay = 0.04f;
                draft.MaxFlickerDelay = 0.25f;
                draft.DamageSeverity = 0.85f;
                draft.EnableKelvin = true;
                draft.KelvinTemperature = 3400;
                break;

            case UltimateLightManager.LightPreset.SciFi:
                draft.Brightness = 1.6f;
                draft.LightColor = new Color( 0.35f, 0.78f, 1.0f );
                draft.SecondaryColor = new Color( 0.1f, 1.0f, 0.8f );
                draft.EnableColorTransition = true;
                draft.ColorTransitionSpeed = 1.15f;
                draft.EnablePulse = true;
                draft.PulseSpeed = 0.85f;
                draft.PulseMin = 0.55f;
                draft.VolumetricBoost = 2.0f;
                break;

            case UltimateLightManager.LightPreset.StreetLight:
                draft.Brightness = 1.4f;
                draft.LightColor = new Color( 1.0f, 0.84f, 0.68f );
                draft.EnableKelvin = true;
                draft.KelvinTemperature = 3500;
                draft.MaxDistance = 4500.0f;
                draft.ShadowMaxDistance = 1200.0f;
                draft.VolumetricBoost = 0.55f;
                break;

            case UltimateLightManager.LightPreset.Custom:
            default:
                break;
        }
    }

    private static void ResetDraftLookSettings( UltimateLightManagerBuilderDraft draft )
    {
        var defaults = new UltimateLightManagerPresetData();

        draft.LightColor = defaults.LightColor;
        draft.SecondaryColor = defaults.SecondaryColor;
        draft.Brightness = defaults.Brightness;
        draft.VolumetricBoost = defaults.VolumetricBoost;
        draft.CastShadows = defaults.CastShadows;
        draft.EnableKelvin = defaults.EnableKelvin;
        draft.KelvinTemperature = defaults.KelvinTemperature;
        draft.EnableFire = defaults.EnableFire;
        draft.FireSpeed = defaults.FireSpeed;
        draft.FireIntensity = defaults.FireIntensity;
        draft.FireChaos = defaults.FireChaos;
        draft.EnableHorror = defaults.EnableHorror;
        draft.MinFlickerDelay = defaults.MinFlickerDelay;
        draft.MaxFlickerDelay = defaults.MaxFlickerDelay;
        draft.DamageSeverity = defaults.DamageSeverity;
        draft.EnableColorTransition = defaults.EnableColorTransition;
        draft.ColorTransitionSpeed = defaults.ColorTransitionSpeed;
        draft.EnablePulse = defaults.EnablePulse;
        draft.PulseSpeed = defaults.PulseSpeed;
        draft.PulseMin = defaults.PulseMin;
        draft.EnableStrobe = defaults.EnableStrobe;
        draft.StrobeSpeed = defaults.StrobeSpeed;
        draft.StrobeDutyCycle = defaults.StrobeDutyCycle;
        draft.AlarmColor = defaults.AlarmColor;
        draft.MaxDistance = defaults.MaxDistance;
        draft.ShadowMaxDistance = defaults.ShadowMaxDistance;
    }

    private static void ApplyDraftToLight( UltimateLightManagerBuilderDraft draft, UltimateLightManager light )
    {
        draft.ApplyToLight( light );
    }

    private static UltimateLightManager GetSelectedLightManager()
    {
        foreach ( var light in EditorScene.Selection.OfType<UltimateLightManager>() )
        {
            if ( light != null )
            {
                return light;
            }
        }

        foreach ( var component in EditorScene.Selection.OfType<Component>() )
        {
            if ( component is UltimateLightManager light )
            {
                return light;
            }
        }

        foreach ( var go in EditorScene.Selection.OfType<GameObject>() )
        {
            if ( go == null || go is Scene )
            {
                continue;
            }

            var light = go.Components
                .GetAll<UltimateLightManager>( FindMode.InSelf | FindMode.InDescendants | FindMode.Enabled | FindMode.Disabled )
                .FirstOrDefault();

            if ( light != null )
            {
                return light;
            }
        }

        return null;
    }

    private static GameObject GetSelectedParentGameObject()
    {
        foreach ( var go in EditorScene.Selection.OfType<GameObject>() )
        {
            if ( go != null && go is not Scene )
            {
                return go;
            }
        }

        foreach ( var component in EditorScene.Selection.OfType<Component>() )
        {
            if ( component?.GameObject != null && component.GameObject is not Scene )
            {
                return component.GameObject;
            }
        }

        return null;
    }

    private void SaveState()
    {
        _state.Draft = _draft;
        global::Editor.FileSystem.ProjectSettings.WriteJson( StatePath, _state );
    }

    private static UltimateLightManagerBuilderState LoadState()
    {
        var loaded = global::Editor.FileSystem.ProjectSettings.ReadJsonOrDefault<UltimateLightManagerBuilderState>( StatePath ) ?? new UltimateLightManagerBuilderState();
        UltimateLightManagerToolStateMigration.ApplyDefaults( loaded );

        loaded.Draft ??= new UltimateLightManagerBuilderDraft();
        loaded.CustomPresets ??= new List<UltimateLightManagerCustomPreset>();
        loaded.CustomPresets = loaded.CustomPresets
            .Where( x => x != null && !string.IsNullOrWhiteSpace( x.Name ) )
            .GroupBy( x => x.Name, StringComparer.OrdinalIgnoreCase )
            .Select( x => x.Last() )
            .ToList();
        loaded.SelectedPresetKey ??= GetBuiltInPresetKey( UltimateLightManager.LightPreset.Custom );

        foreach ( var preset in loaded.CustomPresets )
        {
            preset.Data ??= new UltimateLightManagerPresetData();
        }

        return loaded;
    }

    private static string GetBuiltInPresetKey( UltimateLightManager.LightPreset preset )
    {
        return $"builtin:{preset}";
    }

    private static string GetCustomPresetKey( string presetName )
    {
        return $"custom:{presetName?.Trim()}";
    }

    private sealed class PresetEntry
    {
        public string Key { get; init; }
        public bool IsCustom { get; init; }
        public UltimateLightManager.LightPreset BuiltInPreset { get; init; }
        public UltimateLightManagerCustomPreset CustomPreset { get; init; }

        public static PresetEntry ForBuiltIn( UltimateLightManager.LightPreset preset )
        {
            return new PresetEntry
            {
                Key = GetBuiltInPresetKey( preset ),
                BuiltInPreset = preset
            };
        }

        public static PresetEntry ForCustom( UltimateLightManagerCustomPreset preset )
        {
            return new PresetEntry
            {
                Key = GetCustomPresetKey( preset.Name ),
                IsCustom = true,
                CustomPreset = preset
            };
        }
    }
}

public sealed class UltimateLightManagerBuilderState
{
    public UltimateLightManagerBuilderDraft Draft { get; set; } = new();
    public List<UltimateLightManagerCustomPreset> CustomPresets { get; set; } = new();
    public string SelectedPresetKey { get; set; } = string.Empty;
    public string LoadedPresetKey { get; set; } = string.Empty;
    public UltimateLightManagerToolLanguage ToolLanguage { get; set; } = UltimateLightManagerToolLanguage.French;
    public bool ShowHelpTexts { get; set; } = true;
    public bool ShowShortcutSection { get; set; } = true;
    public int ToolSettingsVersion { get; set; } = 2;
}

public sealed class UltimateLightManagerCustomPreset
{
    public string Name { get; set; } = string.Empty;
    public UltimateLightManagerPresetData Data { get; set; } = new();
}

public class UltimateLightManagerPresetData
{
    private static readonly PropertyInfo[] ComparisonProperties = typeof( UltimateLightManagerPresetData )
        .GetProperties( BindingFlags.Instance | BindingFlags.Public )
        .Where( property => property.CanRead && property.CanWrite )
        .ToArray();

    [Property, Group( "Creation" ), Order( -10 ), Title( "Light Type" )]
    public UltimateLightManager.LightTypeEnum TargetLightType { get; set; } = UltimateLightManager.LightTypeEnum.Point;

    [Property, Group( "Preset" ), Order( -10 ), Title( "Builtin Preset" )]
    public UltimateLightManager.LightPreset SelectedPreset { get; set; } = UltimateLightManager.LightPreset.Custom;

    [Property, Group( "Preset" )]
    public bool AutoApplyPreset { get; set; } = true;

    [Property, Group( "Management" )]
    public string LightGroup { get; set; } = "Default";

    [Property, Group( "Management" )]
    public string PowerGridTag { get; set; } = string.Empty;

    [Property, Group( "Management" )]
    public float StartDelay { get; set; } = 0.0f;

    [Property, Group( "Management" )]
    public bool AutoDesync { get; set; } = true;

    [Property, Group( "Management" )]
    public bool ShowDebugGizmos { get; set; } = false;

    [Property, Group( "General" )]
    public bool IsEnabled { get; set; } = true;

    [Property, Group( "General" )]
    public Color LightColor { get; set; } = Color.White;

    [Property, Group( "General" ), Range( 0, 100 )]
    public float Brightness { get; set; } = 1.0f;

    [Property, Group( "General" ), Range( 0, 10 )]
    public float VolumetricBoost { get; set; } = 1.0f;

    [Property, Group( "General" )]
    public bool CastShadows { get; set; } = true;

    [Property, Group( "Transitions" )]
    public bool EnableFade { get; set; } = false;

    [Property, Group( "Transitions" )]
    public float FadeInDuration { get; set; } = 0.2f;

    [Property, Group( "Transitions" )]
    public float FadeOutDuration { get; set; } = 0.2f;

    [Property, Group( "Audio" )]
    public SoundEvent AmbientSound { get; set; }

    [Property, Group( "Audio" )]
    public SoundEvent ToggleOnSound { get; set; }

    [Property, Group( "Audio" )]
    public SoundEvent ToggleOffSound { get; set; }

    [Property, Group( "Audio" )]
    public bool ModulateVolumeWithLight { get; set; } = true;

    [Property, Group( "Audio" )]
    public bool ModulatePitchWithLight { get; set; } = false;

    [Property, Group( "Audio" ), Range( 0, 5 )]
    public float BaseVolume { get; set; } = 1.0f;

    [Property, Group( "Audio" ), Range( 0.5f, 2f )]
    public float MinPitch { get; set; } = 0.9f;

    [Property, Group( "Audio" ), Range( 0.5f, 2f )]
    public float MaxPitch { get; set; } = 1.1f;

    [Property, Group( "Optimization" )]
    public float MaxDistance { get; set; } = 2500.0f;

    [Property, Group( "Optimization" )]
    public float ShadowMaxDistance { get; set; } = 800.0f;

    [Property, Group( "Optimization" )]
    public bool EnableCulling { get; set; } = true;

    [Property, Group( "Optimization" )]
    public bool EnableAdaptiveUpdates { get; set; } = false;

    [Property, Group( "Optimization" ), Range( 1, 120 )]
    public float NearUpdateRate { get; set; } = 60.0f;

    [Property, Group( "Optimization" ), Range( 1, 120 )]
    public float FarUpdateRate { get; set; } = 12.0f;

    [Property, Group( "Gameplay" )]
    public float DefaultAlarmDuration { get; set; } = 2.0f;

    [Property, Group( "Gameplay" )]
    public Color AlarmColor { get; set; } = new Color( 1.0f, 0.15f, 0.1f );

    [Property, Group( "Gameplay" )]
    public float AlarmStrobeSpeed { get; set; } = 8.0f;

    [Property, Group( "Gameplay" )]
    public float AlarmBrightnessMultiplier { get; set; } = 1.5f;

    [Property, FeatureEnabled( "Fire & Candle" )]
    public bool EnableFire { get; set; } = false;

    [Property, Feature( "Fire & Candle" )]
    public float FireSpeed { get; set; } = 12.0f;

    [Property, Feature( "Fire & Candle" ), Range( 0, 1 )]
    public float FireIntensity { get; set; } = 0.3f;

    [Property, Feature( "Fire & Candle" ), Range( 0, 2 )]
    public float FireChaos { get; set; } = 1.0f;

    [Property, FeatureEnabled( "Horror Mode" )]
    public bool EnableHorror { get; set; } = false;

    [Property, Feature( "Horror Mode" )]
    public float MinFlickerDelay { get; set; } = 0.05f;

    [Property, Feature( "Horror Mode" )]
    public float MaxFlickerDelay { get; set; } = 0.4f;

    [Property, Feature( "Horror Mode" ), Range( 0, 1 )]
    public float DamageSeverity { get; set; } = 0.8f;

    [Property, Feature( "Horror Mode" )]
    public SoundEvent SparkSound { get; set; }

    [Property, FeatureEnabled( "Disco Mode" )]
    public bool EnableDisco { get; set; } = false;

    [Property, Feature( "Disco Mode" )]
    public float DiscoSpeed { get; set; } = 20.0f;

    [Property, Feature( "Disco Mode" ), Range( 0, 1 )]
    public float DiscoSaturation { get; set; } = 1.0f;

    [Property, Feature( "Disco Mode" ), Range( 0, 1 )]
    public float DiscoValue { get; set; } = 1.0f;

    [Property, FeatureEnabled( "Color Transition" )]
    public bool EnableColorTransition { get; set; } = false;

    [Property, Feature( "Color Transition" )]
    public Color SecondaryColor { get; set; } = new Color( 0.2f, 0.85f, 1.0f );

    [Property, Feature( "Color Transition" )]
    public float ColorTransitionSpeed { get; set; } = 1.0f;

    [Property, FeatureEnabled( "Proximity Sensor" )]
    public bool EnableSensor { get; set; } = false;

    [Property, Feature( "Proximity Sensor" )]
    public float SensorRange { get; set; } = 300.0f;

    [Property, Feature( "Proximity Sensor" ), Range( 0, 1 )]
    public float SensorMinBrightness { get; set; } = 0.0f;

    [Property, Feature( "Proximity Sensor" ), Range( 0, 1 )]
    public float SensorMaxBrightness { get; set; } = 1.0f;

    [Property, Feature( "Proximity Sensor" ), Range( 1, 20 )]
    public float SensorSmoothness { get; set; } = 5.0f;

    [Property, Feature( "Proximity Sensor" )]
    public bool InvertSensor { get; set; } = false;

    [Property, FeatureEnabled( "Motion Sway" )]
    public bool EnableSway { get; set; } = false;

    [Property, Feature( "Motion Sway" )]
    public float SwaySpeedPitch { get; set; } = 1.0f;

    [Property, Feature( "Motion Sway" )]
    public float SwayAmountPitch { get; set; } = 5.0f;

    [Property, Feature( "Motion Sway" )]
    public float SwaySpeedRoll { get; set; } = 0.7f;

    [Property, Feature( "Motion Sway" )]
    public float SwayAmountRoll { get; set; } = 3.0f;

    [Property, FeatureEnabled( "Flicker Pattern" )]
    public bool EnablePattern { get; set; } = false;

    [Property, Feature( "Flicker Pattern" )]
    public string Pattern { get; set; } = "mmnmmommommnonmmonqnmmo";

    [Property, Feature( "Flicker Pattern" )]
    public float PatternSpeed { get; set; } = 10.0f;

    [Property, FeatureEnabled( "Pulse" )]
    public bool EnablePulse { get; set; } = false;

    [Property, Feature( "Pulse" )]
    public float PulseSpeed { get; set; } = 1.0f;

    [Property, Feature( "Pulse" ), Range( 0, 1 )]
    public float PulseMin { get; set; } = 0.2f;

    [Property, FeatureEnabled( "Strobe" )]
    public bool EnableStrobe { get; set; } = false;

    [Property, Feature( "Strobe" )]
    public float StrobeSpeed { get; set; } = 10.0f;

    [Property, Feature( "Strobe" ), Range( 0.1f, 0.9f )]
    public float StrobeDutyCycle { get; set; } = 0.5f;

    [Property, FeatureEnabled( "Kelvin" )]
    public bool EnableKelvin { get; set; } = false;

    [Property, Feature( "Kelvin" ), Range( 1000, 12000 )]
    public int KelvinTemperature { get; set; } = 4500;

    [Property, FeatureEnabled( "Power Surge" )]
    public bool EnablePowerSurge { get; set; } = false;

    [Property, Feature( "Power Surge" )]
    public float SurgeMinInterval { get; set; } = 4.0f;

    [Property, Feature( "Power Surge" )]
    public float SurgeMaxInterval { get; set; } = 10.0f;

    [Property, Feature( "Power Surge" )]
    public float SurgeDuration { get; set; } = 0.15f;

    [Property, Feature( "Power Surge" )]
    public float SurgeBrightnessMultiplier { get; set; } = 1.8f;

    public virtual void ApplyPresetData( UltimateLightManagerPresetData data )
    {
        if ( data == null )
        {
            return;
        }

        TargetLightType = data.TargetLightType;
        SelectedPreset = data.SelectedPreset;
        AutoApplyPreset = data.AutoApplyPreset;
        LightGroup = data.LightGroup ?? "Default";
        PowerGridTag = data.PowerGridTag ?? string.Empty;
        StartDelay = data.StartDelay;
        AutoDesync = data.AutoDesync;
        ShowDebugGizmos = data.ShowDebugGizmos;
        IsEnabled = data.IsEnabled;
        LightColor = data.LightColor;
        Brightness = data.Brightness;
        VolumetricBoost = data.VolumetricBoost;
        CastShadows = data.CastShadows;
        EnableFade = data.EnableFade;
        FadeInDuration = data.FadeInDuration;
        FadeOutDuration = data.FadeOutDuration;
        AmbientSound = data.AmbientSound;
        ToggleOnSound = data.ToggleOnSound;
        ToggleOffSound = data.ToggleOffSound;
        ModulateVolumeWithLight = data.ModulateVolumeWithLight;
        ModulatePitchWithLight = data.ModulatePitchWithLight;
        BaseVolume = data.BaseVolume;
        MinPitch = data.MinPitch;
        MaxPitch = data.MaxPitch;
        MaxDistance = data.MaxDistance;
        ShadowMaxDistance = data.ShadowMaxDistance;
        EnableCulling = data.EnableCulling;
        EnableAdaptiveUpdates = data.EnableAdaptiveUpdates;
        NearUpdateRate = data.NearUpdateRate;
        FarUpdateRate = data.FarUpdateRate;
        DefaultAlarmDuration = data.DefaultAlarmDuration;
        AlarmColor = data.AlarmColor;
        AlarmStrobeSpeed = data.AlarmStrobeSpeed;
        AlarmBrightnessMultiplier = data.AlarmBrightnessMultiplier;
        EnableFire = data.EnableFire;
        FireSpeed = data.FireSpeed;
        FireIntensity = data.FireIntensity;
        FireChaos = data.FireChaos;
        EnableHorror = data.EnableHorror;
        MinFlickerDelay = data.MinFlickerDelay;
        MaxFlickerDelay = data.MaxFlickerDelay;
        DamageSeverity = data.DamageSeverity;
        SparkSound = data.SparkSound;
        EnableDisco = data.EnableDisco;
        DiscoSpeed = data.DiscoSpeed;
        DiscoSaturation = data.DiscoSaturation;
        DiscoValue = data.DiscoValue;
        EnableColorTransition = data.EnableColorTransition;
        SecondaryColor = data.SecondaryColor;
        ColorTransitionSpeed = data.ColorTransitionSpeed;
        EnableSensor = data.EnableSensor;
        SensorRange = data.SensorRange;
        SensorMinBrightness = data.SensorMinBrightness;
        SensorMaxBrightness = data.SensorMaxBrightness;
        SensorSmoothness = data.SensorSmoothness;
        InvertSensor = data.InvertSensor;
        EnableSway = data.EnableSway;
        SwaySpeedPitch = data.SwaySpeedPitch;
        SwayAmountPitch = data.SwayAmountPitch;
        SwaySpeedRoll = data.SwaySpeedRoll;
        SwayAmountRoll = data.SwayAmountRoll;
        EnablePattern = data.EnablePattern;
        Pattern = data.Pattern ?? string.Empty;
        PatternSpeed = data.PatternSpeed;
        EnablePulse = data.EnablePulse;
        PulseSpeed = data.PulseSpeed;
        PulseMin = data.PulseMin;
        EnableStrobe = data.EnableStrobe;
        StrobeSpeed = data.StrobeSpeed;
        StrobeDutyCycle = data.StrobeDutyCycle;
        EnableKelvin = data.EnableKelvin;
        KelvinTemperature = data.KelvinTemperature;
        EnablePowerSurge = data.EnablePowerSurge;
        SurgeMinInterval = data.SurgeMinInterval;
        SurgeMaxInterval = data.SurgeMaxInterval;
        SurgeDuration = data.SurgeDuration;
        SurgeBrightnessMultiplier = data.SurgeBrightnessMultiplier;
    }

    public virtual void CaptureFromLight( UltimateLightManager light )
    {
        if ( light == null )
        {
            return;
        }

        TargetLightType = light.TargetLightType;
        SelectedPreset = light.SelectedPreset;
        AutoApplyPreset = light.AutoApplyPreset;
        LightGroup = light.LightGroup;
        PowerGridTag = light.PowerGridTag;
        StartDelay = light.StartDelay;
        AutoDesync = light.AutoDesync;
        ShowDebugGizmos = light.ShowDebugGizmos;
        IsEnabled = light.IsEnabled;
        LightColor = light.LightColor;
        Brightness = light.Brightness;
        VolumetricBoost = light.VolumetricBoost;
        CastShadows = light.CastShadows;
        EnableFade = light.EnableFade;
        FadeInDuration = light.FadeInDuration;
        FadeOutDuration = light.FadeOutDuration;
        AmbientSound = light.AmbientSound;
        ToggleOnSound = light.ToggleOnSound;
        ToggleOffSound = light.ToggleOffSound;
        ModulateVolumeWithLight = light.ModulateVolumeWithLight;
        ModulatePitchWithLight = light.ModulatePitchWithLight;
        BaseVolume = light.BaseVolume;
        MinPitch = light.MinPitch;
        MaxPitch = light.MaxPitch;
        MaxDistance = light.MaxDistance;
        ShadowMaxDistance = light.ShadowMaxDistance;
        EnableCulling = light.EnableCulling;
        EnableAdaptiveUpdates = light.EnableAdaptiveUpdates;
        NearUpdateRate = light.NearUpdateRate;
        FarUpdateRate = light.FarUpdateRate;
        DefaultAlarmDuration = light.DefaultAlarmDuration;
        AlarmColor = light.AlarmColor;
        AlarmStrobeSpeed = light.AlarmStrobeSpeed;
        AlarmBrightnessMultiplier = light.AlarmBrightnessMultiplier;
        EnableFire = light.EnableFire;
        FireSpeed = light.FireSpeed;
        FireIntensity = light.FireIntensity;
        FireChaos = light.FireChaos;
        EnableHorror = light.EnableHorror;
        MinFlickerDelay = light.MinFlickerDelay;
        MaxFlickerDelay = light.MaxFlickerDelay;
        DamageSeverity = light.DamageSeverity;
        SparkSound = light.SparkSound;
        EnableDisco = light.EnableDisco;
        DiscoSpeed = light.DiscoSpeed;
        DiscoSaturation = light.DiscoSaturation;
        DiscoValue = light.DiscoValue;
        EnableColorTransition = light.EnableColorTransition;
        SecondaryColor = light.SecondaryColor;
        ColorTransitionSpeed = light.ColorTransitionSpeed;
        EnableSensor = light.EnableSensor;
        SensorRange = light.SensorRange;
        SensorMinBrightness = light.SensorMinBrightness;
        SensorMaxBrightness = light.SensorMaxBrightness;
        SensorSmoothness = light.SensorSmoothness;
        InvertSensor = light.InvertSensor;
        EnableSway = light.EnableSway;
        SwaySpeedPitch = light.SwaySpeedPitch;
        SwayAmountPitch = light.SwayAmountPitch;
        SwaySpeedRoll = light.SwaySpeedRoll;
        SwayAmountRoll = light.SwayAmountRoll;
        EnablePattern = light.EnablePattern;
        Pattern = light.Pattern;
        PatternSpeed = light.PatternSpeed;
        EnablePulse = light.EnablePulse;
        PulseSpeed = light.PulseSpeed;
        PulseMin = light.PulseMin;
        EnableStrobe = light.EnableStrobe;
        StrobeSpeed = light.StrobeSpeed;
        StrobeDutyCycle = light.StrobeDutyCycle;
        EnableKelvin = light.EnableKelvin;
        KelvinTemperature = light.KelvinTemperature;
        EnablePowerSurge = light.EnablePowerSurge;
        SurgeMinInterval = light.SurgeMinInterval;
        SurgeMaxInterval = light.SurgeMaxInterval;
        SurgeDuration = light.SurgeDuration;
        SurgeBrightnessMultiplier = light.SurgeBrightnessMultiplier;
    }

    public virtual void ApplyToLight( UltimateLightManager light )
    {
        if ( light == null )
        {
            return;
        }

        light.TargetLightType = TargetLightType;
        light.SelectedPreset = SelectedPreset;
        light.AutoApplyPreset = AutoApplyPreset;
        light.LightGroup = LightGroup ?? "Default";
        light.PowerGridTag = PowerGridTag ?? string.Empty;
        light.StartDelay = StartDelay;
        light.AutoDesync = AutoDesync;
        light.ShowDebugGizmos = ShowDebugGizmos;
        light.IsEnabled = IsEnabled;
        light.LightColor = LightColor;
        light.Brightness = Brightness;
        light.VolumetricBoost = VolumetricBoost;
        light.CastShadows = CastShadows;
        light.EnableFade = EnableFade;
        light.FadeInDuration = FadeInDuration;
        light.FadeOutDuration = FadeOutDuration;
        light.AmbientSound = AmbientSound;
        light.ToggleOnSound = ToggleOnSound;
        light.ToggleOffSound = ToggleOffSound;
        light.ModulateVolumeWithLight = ModulateVolumeWithLight;
        light.ModulatePitchWithLight = ModulatePitchWithLight;
        light.BaseVolume = BaseVolume;
        light.MinPitch = MinPitch;
        light.MaxPitch = MaxPitch;
        light.MaxDistance = MaxDistance;
        light.ShadowMaxDistance = ShadowMaxDistance;
        light.EnableCulling = EnableCulling;
        light.EnableAdaptiveUpdates = EnableAdaptiveUpdates;
        light.NearUpdateRate = NearUpdateRate;
        light.FarUpdateRate = FarUpdateRate;
        light.DefaultAlarmDuration = DefaultAlarmDuration;
        light.AlarmColor = AlarmColor;
        light.AlarmStrobeSpeed = AlarmStrobeSpeed;
        light.AlarmBrightnessMultiplier = AlarmBrightnessMultiplier;
        light.EnableFire = EnableFire;
        light.FireSpeed = FireSpeed;
        light.FireIntensity = FireIntensity;
        light.FireChaos = FireChaos;
        light.EnableHorror = EnableHorror;
        light.MinFlickerDelay = MinFlickerDelay;
        light.MaxFlickerDelay = MaxFlickerDelay;
        light.DamageSeverity = DamageSeverity;
        light.SparkSound = SparkSound;
        light.EnableDisco = EnableDisco;
        light.DiscoSpeed = DiscoSpeed;
        light.DiscoSaturation = DiscoSaturation;
        light.DiscoValue = DiscoValue;
        light.EnableColorTransition = EnableColorTransition;
        light.SecondaryColor = SecondaryColor;
        light.ColorTransitionSpeed = ColorTransitionSpeed;
        light.EnableSensor = EnableSensor;
        light.SensorRange = SensorRange;
        light.SensorMinBrightness = SensorMinBrightness;
        light.SensorMaxBrightness = SensorMaxBrightness;
        light.SensorSmoothness = SensorSmoothness;
        light.InvertSensor = InvertSensor;
        light.EnableSway = EnableSway;
        light.SwaySpeedPitch = SwaySpeedPitch;
        light.SwayAmountPitch = SwayAmountPitch;
        light.SwaySpeedRoll = SwaySpeedRoll;
        light.SwayAmountRoll = SwayAmountRoll;
        light.EnablePattern = EnablePattern;
        light.Pattern = Pattern ?? string.Empty;
        light.PatternSpeed = PatternSpeed;
        light.EnablePulse = EnablePulse;
        light.PulseSpeed = PulseSpeed;
        light.PulseMin = PulseMin;
        light.EnableStrobe = EnableStrobe;
        light.StrobeSpeed = StrobeSpeed;
        light.StrobeDutyCycle = StrobeDutyCycle;
        light.EnableKelvin = EnableKelvin;
        light.KelvinTemperature = KelvinTemperature;
        light.EnablePowerSurge = EnablePowerSurge;
        light.SurgeMinInterval = SurgeMinInterval;
        light.SurgeMaxInterval = SurgeMaxInterval;
        light.SurgeDuration = SurgeDuration;
        light.SurgeBrightnessMultiplier = SurgeBrightnessMultiplier;
    }

    public void NormalizeForCustomPreset()
    {
        SelectedPreset = UltimateLightManager.LightPreset.Custom;
        AutoApplyPreset = false;
    }

    public UltimateLightManagerPresetData Clone()
    {
        var copy = new UltimateLightManagerPresetData();
        copy.ApplyPresetData( this );
        return copy;
    }

    public bool ContentEquals( UltimateLightManagerPresetData other )
    {
        if ( other == null )
        {
            return false;
        }

        foreach ( var property in ComparisonProperties )
        {
            var left = property.GetValue( this );
            var right = property.GetValue( other );

            if ( !AreValuesEqual( property.PropertyType, property.Name, left, right ) )
            {
                return false;
            }
        }

        return true;
    }

    private static bool AreValuesEqual( Type propertyType, string propertyName, object left, object right )
    {
        if ( propertyType == typeof( float ) )
        {
            return Math.Abs( (float)(left ?? 0.0f) - (float)(right ?? 0.0f) ) <= 0.0001f;
        }

        if ( propertyType == typeof( string ) )
        {
            return string.Equals(
                NormalizeStringValue( propertyName, left as string ),
                NormalizeStringValue( propertyName, right as string ),
                StringComparison.Ordinal
            );
        }

        return Equals( left, right );
    }

    private static string NormalizeStringValue( string propertyName, string value )
    {
        value ??= string.Empty;

        if ( propertyName == nameof( LightGroup ) )
        {
            value = string.IsNullOrWhiteSpace( value ) ? "Default" : value;
        }

        return value;
    }
}

public sealed class UltimateLightManagerBuilderDraft : UltimateLightManagerPresetData
{
    [Property, Group( "Creation" ), Order( -20 ), Title( "Light Name" )]
    public string LightName { get; set; } = "Ultimate Light";

    public string GetSafeLightName()
    {
        return string.IsNullOrWhiteSpace( LightName ) ? "Ultimate Light" : LightName.Trim();
    }

    public override void ApplyPresetData( UltimateLightManagerPresetData data )
    {
        base.ApplyPresetData( data );
    }

    public UltimateLightManagerPresetData ToPresetData()
    {
        var copy = Clone();
        copy.NormalizeForCustomPreset();
        return copy;
    }
}