Editor/UltimateLightManagerEditorTool.cs

Editor tool UI and actions for the Ultimate Light Manager. Defines an editor tool with move/rotate/scale subtools, a sidebar window and panel that build the UI for creating, selecting and managing light presets, and a static actions class that loads/saves builder state, applies presets and manipulates scene objects.

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

namespace Dreams.UltimateLightManager.Editor;

[EditorTool( "tools.ultimate-light-manager" )]
[Title( "Ultimate Light Manager" )]
[Icon( "tungsten" )]
[Group( "Scene" )]
[Order( -9998 )]
internal sealed class UltimateLightManagerEditorTool : ObjectEditorTool
{
    private UltimateLightManagerToolWindow _window;

    public UltimateLightManagerEditorTool()
    {
        RebuildSidebarOnSelectionChange = false;
    }

    public override IEnumerable<EditorTool> GetSubtools()
    {
        yield return new UltimateLightManagerPositionTool();
        yield return new UltimateLightManagerRotationTool();
        yield return new UltimateLightManagerScaleTool();
    }

    public override Widget CreateToolSidebar() => null;

    public override void OnEnabled()
    {
        base.OnEnabled();

        _window = new UltimateLightManagerToolWindow( this );
        AddOverlay( _window, TextFlag.RightTop, 10 );
        _window.RefreshAll();
    }

    public override void OnSelectionChanged()
    {
        base.OnSelectionChanged();
        _window?.RefreshSelection();
    }

    public override void BuildSceneContextMenu( Menu menu, Ray ray, SceneTraceResult? trace )
    {
        if ( !TryGetSpawnPositionFromRay( ray, trace, out var worldPosition ) )
        {
            return;
        }

        var texts = UltimateLightManagerToolTexts.For( UltimateLightManagerEditorActions.LoadState().ToolLanguage );
        menu.AddSeparator();
        menu.AddOption( texts["scene.create_here"], "tungsten", () =>
        {
            var state = UltimateLightManagerEditorActions.LoadState();
            UltimateLightManagerEditorActions.CreateLightFromDraft( state.Draft, worldPosition, GetSpawnRotation( ray.Forward ) );
        } );
    }

    private bool TryGetSpawnPositionFromRay( Ray ray, SceneTraceResult? trace, out Vector3 worldPosition )
    {
        if ( trace is { } hit && hit.Hit )
        {
            worldPosition = hit.HitPosition + hit.Normal * 2.0f;
            return true;
        }

        if ( Scene != null )
        {
            var sceneTrace = Scene.Trace.Ray( ray, Gizmo.RayDepth )
                .UseRenderMeshes( true )
                .WithoutTags( "hidden" )
                .UsePhysicsWorld( false )
                .Run();

            if ( sceneTrace.Hit )
            {
                worldPosition = sceneTrace.HitPosition + sceneTrace.Normal * 2.0f;
                return true;
            }
        }

        var plane = new Plane( Vector3.Up, 0.0f );
        if ( plane.TryTrace( ray, out var point, true, Gizmo.RayDepth ) )
        {
            worldPosition = point;
            return true;
        }

        if ( ray.Forward.Length.AlmostEqual( 0.0f ) )
        {
            worldPosition = default;
            return false;
        }

        worldPosition = ray.Position + ray.Forward * 300.0f;
        return true;
    }

    private Rotation GetSpawnRotation( Vector3 forward )
    {
        if ( Camera.IsValid() )
        {
            return Camera.WorldRotation;
        }

        return forward.Length.AlmostEqual( 0.0f ) ? Rotation.Identity : Rotation.LookAt( forward, Vector3.Up );
    }

    [Shortcut( "tools.ultimate-light-manager", "u", typeof( SceneViewWidget ) )]
    public static void ActivateTool()
    {
        EditorToolManager.SetTool( nameof( UltimateLightManagerEditorTool ) );
    }
}

[Title( "Move" )]
[Icon( "control_camera" )]
[Group( "1" )]
[Order( 0 )]
internal sealed class UltimateLightManagerPositionTool : PositionEditorTool
{
    [Shortcut( "tools.ultimate-light-manager.move", "w", typeof( SceneViewWidget ) )]
    public static new void ActivateSubTool()
    {
        if ( EditorToolManager.CurrentModeName != nameof( UltimateLightManagerEditorTool ) )
        {
            return;
        }

        EditorToolManager.SetSubTool( nameof( UltimateLightManagerPositionTool ) );
    }
}

[Title( "Rotate" )]
[Icon( "360" )]
[Group( "1" )]
[Order( 1 )]
internal sealed class UltimateLightManagerRotationTool : RotationEditorTool
{
    [Shortcut( "tools.ultimate-light-manager.rotate", "e", typeof( SceneViewWidget ) )]
    public static new void ActivateSubTool()
    {
        if ( EditorToolManager.CurrentModeName != nameof( UltimateLightManagerEditorTool ) )
        {
            return;
        }

        EditorToolManager.SetSubTool( nameof( UltimateLightManagerRotationTool ) );
    }
}

[Title( "Scale" )]
[Icon( "zoom_out_map" )]
[Group( "1" )]
[Order( 2 )]
internal sealed class UltimateLightManagerScaleTool : ScaleEditorTool
{
    [Shortcut( "tools.ultimate-light-manager.scale", "r", typeof( SceneViewWidget ) )]
    public static new void ActivateSubTool()
    {
        if ( EditorToolManager.CurrentModeName != nameof( UltimateLightManagerEditorTool ) )
        {
            return;
        }

        EditorToolManager.SetSubTool( nameof( UltimateLightManagerScaleTool ) );
    }
}

internal sealed class UltimateLightManagerToolWindow : WidgetWindow
{
    private readonly UltimateLightManagerToolPanel _panel;

    public UltimateLightManagerToolWindow( UltimateLightManagerEditorTool tool ) : base( tool.SceneOverlay, "Ultimate Light Manager" )
    {
        Icon = "tungsten";
        Size = new Vector2( 460, 760 );
        MinimumWidth = 380;
        MinimumHeight = 520;

        Layout = Layout.Column();
        Layout.Margin = 0;
        Layout.Spacing = 0;

        _panel = new UltimateLightManagerToolPanel( tool, this );
        Layout.Add( _panel, 1 );
    }

    public void RefreshAll()
    {
        _panel.Rebuild();
    }

    public void RefreshSelection()
    {
        _panel.RefreshSelection();
    }
}

internal sealed class UltimateLightManagerToolPanel : ToolSidebarWidget
{
    private readonly UltimateLightManagerEditorTool _tool;
    private readonly UltimateLightManagerBuilderState _state;
    private readonly UltimateLightManagerBuilderDraft _draft;
    private readonly SerializedObject _draftObject;
    private readonly List<PresetEntry> _presetEntries = new();
    private readonly UltimateLightManagerToolLanguage[] _languageOptions = Enum.GetValues<UltimateLightManagerToolLanguage>();

    private ComboBox _presetCombo;
    private ComboBox _languageCombo;
    private Label _presetStatusLabel;
    private Button _savePresetButton;
    private Button _duplicatePresetButton;
    private Button _renamePresetButton;
    private Button _deletePresetButton;
    private Button _revertPresetButton;
    private bool _isUpdatingPresetCombo;
    private bool _isUpdatingLanguageCombo;

    private UltimateLightManagerToolTexts Texts => UltimateLightManagerToolTexts.For( _state.ToolLanguage );

    public UltimateLightManagerToolPanel( UltimateLightManagerEditorTool tool, Widget parent = null ) : base( parent )
    {
        _tool = tool;
        _state = UltimateLightManagerEditorActions.LoadState();
        _draft = _state.Draft ?? new UltimateLightManagerBuilderDraft();
        _state.Draft = _draft;

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

        Layout.Margin = 0;
        Layout.Spacing = 0;

        BuildUi();
    }

    protected override void OnPaint()
    {
    }

    public void Rebuild()
    {
        BuildUi();
    }

    public void RefreshSelection()
    {
        BuildUi();
    }

    private void BuildUi()
    {
        Layout.Clear( true );

        if ( Parent is WidgetWindow window )
        {
            window.WindowTitle = T( "window.title" );
        }

        var tabs = Layout.Add( new TabWidget( this ), 1 );
        tabs.ShowText = true;

        var creationPage = AddPage( tabs, T( "tab.creation" ), "add_circle" );
        BuildCreationPage( creationPage );

        var selectionPage = AddPage( tabs, T( "tab.selection" ), "tune" );
        BuildSelectionPage( selectionPage );

        var groupsPage = AddPage( tabs, T( "tab.groups" ), "groups" );
        BuildGroupsPage( groupsPage );

        var settingsPage = AddPage( tabs, T( "tab.settings" ), "settings" );
        BuildSettingsPage( settingsPage );

        tabs.StateCookie = "ultimate-light-manager.tool-window";
    }

    private static UltimateLightManagerPage AddPage( TabWidget tabs, string title, string icon )
    {
        var scroll = new ScrollArea( tabs );
        var page = new UltimateLightManagerPage( scroll );
        scroll.Canvas = page;
        tabs.AddPage( title, icon, scroll );
        return page;
    }

    private void BuildCreationPage( UltimateLightManagerPage page )
    {
        var group = page.AddGroup( T( "group.creation" ), SizeMode.Flexible );

        AddOptionalHint( group, "hint.creation_intro", page );

        var presetRow = group.AddRow();
        presetRow.Spacing = 6;
        presetRow.Add( new Label( T( "label.preset" ), page ) { FixedWidth = 60, Alignment = TextFlag.LeftCenter } );

        _presetCombo = presetRow.Add( new ComboBox( page ), 1 );
        _presetCombo.ItemChanged += OnPresetSelectionChanged;

        presetRow.Add( new Button( T( "button.from_selection" ), page )
        {
            Icon = "south_west",
            Clicked = LoadFromSelection,
            Enabled = UltimateLightManagerEditorActions.GetSelectedLightManagers().Length > 0
        } );

        RebuildPresetCombo();

        _presetStatusLabel = new Label( GetPresetStatusText(), page )
        {
            WordWrap = true
        };
        _presetStatusLabel.SetStyles( $"color: {Theme.Text.WithAlpha( 0.7f ).Hex};" );
        group.Add( _presetStatusLabel );

        AddOptionalHint( group, "hint.preset_workflow", page, 0.6f );

        var primaryActionRow = group.AddRow();
        primaryActionRow.Spacing = 6;
        primaryActionRow.Add( _savePresetButton = new Button( T( "button.save" ), page )
        {
            Icon = "save",
            Clicked = HandleSavePreset
        }, 1 );
        primaryActionRow.Add( _duplicatePresetButton = new Button( T( "button.duplicate" ), page )
        {
            Icon = "content_copy",
            Clicked = OpenDuplicatePresetDialog
        }, 1 );
        primaryActionRow.Add( _revertPresetButton = new Button( T( "button.revert" ), page )
        {
            Icon = "restore",
            Clicked = RevertToLoadedPreset
        }, 1 );

        var secondaryActionRow = group.AddRow();
        secondaryActionRow.Spacing = 6;
        secondaryActionRow.Add( _renamePresetButton = new Button( T( "button.rename" ), page )
        {
            Icon = "drive_file_rename_outline",
            Clicked = OpenRenamePresetDialog
        }, 1 );
        secondaryActionRow.Add( _deletePresetButton = new Button( T( "button.delete" ), page )
        {
            Icon = "delete",
            Clicked = ConfirmDeleteSelectedPreset
        }, 1 );
        secondaryActionRow.Add( new Button( T( "button.reset" ), page )
        {
            Icon = "restart_alt",
            Clicked = ResetDraft
        }, 1 );

        RefreshPresetWorkflowUi();

        var sheet = new ControlSheet();
        sheet.AddObject( _draftObject, ShouldShowDraftProperty );
        group.Add( sheet );

        AddOptionalHint( group, "hint.create_center", page, 0.6f );
    }

    private void BuildSelectionPage( UltimateLightManagerPage page )
    {
        var lights = UltimateLightManagerEditorActions.GetSelectedLightManagers();

        var manipulationGroup = page.AddGroup( T( "group.manipulation" ) );
        AddOptionalHint( manipulationGroup, "hint.manipulation", page );

        var toolRow = manipulationGroup.AddRow();
        toolRow.Spacing = 6;
        toolRow.Add( new Button( T( "button.move" ), page )
        {
            Icon = "control_camera",
            Clicked = () => EditorToolManager.SetSubTool( nameof( UltimateLightManagerPositionTool ) ),
            Enabled = lights.Length > 0
        }, 1 );
        toolRow.Add( new Button( T( "button.rotate" ), page )
        {
            Icon = "360",
            Clicked = () => EditorToolManager.SetSubTool( nameof( UltimateLightManagerRotationTool ) ),
            Enabled = lights.Length > 0
        }, 1 );
        toolRow.Add( new Button( T( "button.scale" ), page )
        {
            Icon = "zoom_out_map",
            Clicked = () => EditorToolManager.SetSubTool( nameof( UltimateLightManagerScaleTool ) ),
            Enabled = lights.Length > 0
        }, 1 );

        manipulationGroup.Add( new Label( TF( "label.active_mode_format", GetCurrentTransformModeName() ), page ) );

        var selectionGroup = page.AddGroup( T( "group.selection" ), SizeMode.Flexible );

        selectionGroup.Add( new Label( GetSelectionSummaryText( lights.Length ), page ) { WordWrap = true } );

        if ( lights.Length == 0 )
        {
            AddOptionalHint( selectionGroup, "hint.no_selection", page, 0.6f );
            return;
        }

        var groupName = UltimateLightManagerEditorActions.GetSelectionSummary( lights, x => x.LightGroup );
        var gridName = UltimateLightManagerEditorActions.GetSelectionSummary( lights, x => x.PowerGridTag );

        if ( !string.IsNullOrWhiteSpace( groupName ) )
        {
            selectionGroup.Add( new Label( TF( "label.group_format", groupName ), page ) );
        }

        if ( !string.IsNullOrWhiteSpace( gridName ) )
        {
            selectionGroup.Add( new Label( TF( "label.power_grid_format", gridName ), page ) );
        }

        selectionGroup.Add( new Button( T( "button.apply_draft_selection" ), page )
        {
            Icon = "playlist_add_check",
            Clicked = () => UltimateLightManagerEditorActions.ApplyDraftToSelection( _draft )
        } );

        var quickActions = selectionGroup.AddRow();
        quickActions.Spacing = 6;
        quickActions.Add( new Button( T( "button.on" ), page )
        {
            Icon = "toggle_on",
            Clicked = () => UltimateLightManagerEditorActions.ApplyToSelection( "Turn On Light(s)", light => light.TurnOn() )
        }, 1 );
        quickActions.Add( new Button( T( "button.off" ), page )
        {
            Icon = "toggle_off",
            Clicked = () => UltimateLightManagerEditorActions.ApplyToSelection( "Turn Off Light(s)", light => light.TurnOff() )
        }, 1 );
        quickActions.Add( new Button( T( "button.toggle" ), page )
        {
            Icon = "sync_alt",
            Clicked = () => UltimateLightManagerEditorActions.ApplyToSelection( "Toggle Light(s)", light => light.Toggle() )
        }, 1 );

        var inspectorGroup = page.AddGroup( T( "group.parameters" ), SizeMode.Flexible );
        AddOptionalHint( inspectorGroup, "hint.parameters", page, 0.6f );

        var selectionObject = GetSelectionObject( lights );
        if ( selectionObject != null )
        {
            var sheet = new ControlSheet();
            sheet.AddObject( selectionObject );
            inspectorGroup.Add( sheet );
        }
    }

    private void BuildGroupsPage( UltimateLightManagerPage page )
    {
        var lights = UltimateLightManagerEditorActions.GetSelectedLightManagers();
        var firstLight = lights.FirstOrDefault();

        var group = page.AddGroup( T( "group.groups" ) );
        AddOptionalHint( group, "hint.groups", page );

        group.Add( new Button( T( "button.assign_group" ), page )
        {
            Icon = "groups",
            Clicked = OpenAssignGroupDialog,
            Enabled = lights.Length > 0
        } );

        group.Add( new Button( T( "button.assign_power_grid" ), page )
        {
            Icon = "electrical_services",
            Clicked = OpenAssignPowerGridDialog,
            Enabled = lights.Length > 0
        } );

        var selectionRow = group.AddRow();
        selectionRow.Spacing = 6;
        selectionRow.Add( new Button( T( "button.select_same_group" ), page )
        {
            Icon = "group_work",
            Clicked = UltimateLightManagerEditorActions.SelectSameGroup,
            Enabled = firstLight != null && !string.IsNullOrWhiteSpace( firstLight.LightGroup )
        }, 1 );
        selectionRow.Add( new Button( T( "button.select_same_grid" ), page )
        {
            Icon = "hub",
            Clicked = UltimateLightManagerEditorActions.SelectSamePowerGrid,
            Enabled = firstLight != null && !string.IsNullOrWhiteSpace( firstLight.PowerGridTag )
        }, 1 );

        var gameplay = page.AddGroup( T( "group.gameplay" ) );
        AddOptionalHint( gameplay, "hint.gameplay", page );

        var example = new Label( T( "example.group_state" ), page )
        {
            WordWrap = true
        };
        example.SetStyles( $"color: {Theme.Text.WithAlpha( 0.55f ).Hex};" );
        gameplay.Add( example );

        if ( _state.ShowShortcutSection )
        {
            page.AddShortcuts(
                ( T( "shortcut.activate_tool" ), "U" ),
                ( T( "shortcut.move_gizmo" ), "W" ),
                ( T( "shortcut.rotate_gizmo" ), "E" ),
                ( T( "shortcut.scale_gizmo" ), "R" ),
                ( T( "shortcut.create_here" ), T( "shortcut.right_click" ) )
            );
        }
    }

    private void BuildSettingsPage( UltimateLightManagerPage page )
    {
        var settingsGroup = page.AddGroup( T( "group.settings" ) );
        AddOptionalHint( settingsGroup, "hint.settings", page );

        var languageRow = settingsGroup.AddRow();
        languageRow.Spacing = 6;
        languageRow.Add( new Label( T( "label.language" ), page ) { FixedWidth = 90, Alignment = TextFlag.LeftCenter } );

        _languageCombo = languageRow.Add( new ComboBox( page ), 1 );
        _languageCombo.ItemChanged += OnLanguageSelectionChanged;

        RebuildLanguageCombo();

        settingsGroup.Add( new Label( TF( "settings.current_language_format", Texts.GetLanguageName( _state.ToolLanguage ) ), page ) { WordWrap = true } );

        var displayGroup = page.AddGroup( T( "group.display" ) );

        var helpRow = displayGroup.AddRow();
        helpRow.Spacing = 6;
        helpRow.Add( new Label( T( "label.help_texts" ), page ) { Alignment = TextFlag.LeftCenter }, 1 );
        helpRow.Add( new Button( _state.ShowHelpTexts ? T( "settings.help_on" ) : T( "settings.help_off" ), page )
        {
            Icon = _state.ShowHelpTexts ? "visibility" : "visibility_off",
            Clicked = ToggleHelpTexts
        } );

        var shortcutsRow = displayGroup.AddRow();
        shortcutsRow.Spacing = 6;
        shortcutsRow.Add( new Label( T( "label.shortcuts_panel" ), page ) { Alignment = TextFlag.LeftCenter }, 1 );
        shortcutsRow.Add( new Button( _state.ShowShortcutSection ? T( "settings.shortcuts_on" ) : T( "settings.shortcuts_off" ), page )
        {
            Icon = _state.ShowShortcutSection ? "keyboard_command_key" : "disabled_by_default",
            Clicked = ToggleShortcutSection
        } );
    }

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

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

        UltimateLightManagerEditorActions.SaveState( _state );
        RefreshPresetWorkflowUi();
        BuildUi();
    }

    private void OnDraftPropertyChanged( SerializedProperty property )
    {
        UltimateLightManagerEditorActions.SaveState( _state );
        RefreshPresetWorkflowUi();
    }

    private void OnLanguageSelectionChanged()
    {
        if ( _isUpdatingLanguageCombo )
        {
            return;
        }

        int index = _languageCombo?.CurrentIndex ?? -1;
        if ( index < 0 || index >= _languageOptions.Length )
        {
            return;
        }

        _state.ToolLanguage = _languageOptions[index];
        UltimateLightManagerEditorActions.SaveState( _state );
        BuildUi();
    }

    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;
        _state.LoadedPresetKey = entry.Key;

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

        UltimateLightManagerEditorActions.SaveState( _state );
        BuildUi();
    }

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

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

        AddBuiltInPreset( UltimateLightManager.LightPreset.Custom );
        AddBuiltInPreset( UltimateLightManager.LightPreset.Candle );
        AddBuiltInPreset( UltimateLightManager.LightPreset.Torch );
        AddBuiltInPreset( UltimateLightManager.LightPreset.Neon );
        AddBuiltInPreset( UltimateLightManager.LightPreset.Alarm );
        AddBuiltInPreset( UltimateLightManager.LightPreset.BrokenLamp );
        AddBuiltInPreset( UltimateLightManager.LightPreset.SciFi );
        AddBuiltInPreset( UltimateLightManager.LightPreset.StreetLight );

        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( Texts.GetCustomPresetLabel( preset.Name ) );
        }

        string selectedKey = _state.SelectedPresetKey;
        if ( string.IsNullOrWhiteSpace( selectedKey ) )
        {
            selectedKey = UltimateLightManagerEditorActions.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 RebuildLanguageCombo()
    {
        if ( _languageCombo == null )
        {
            return;
        }

        _isUpdatingLanguageCombo = true;
        _languageCombo.Clear();

        foreach ( var language in _languageOptions )
        {
            _languageCombo.AddItem( Texts.GetLanguageName( language ) );
        }

        _languageCombo.CurrentIndex = Array.IndexOf( _languageOptions, _state.ToolLanguage );
        _isUpdatingLanguageCombo = false;
    }

    private void AddBuiltInPreset( UltimateLightManager.LightPreset preset )
    {
        _presetEntries.Add( PresetEntry.ForBuiltIn( preset ) );
        _presetCombo.AddItem( Texts.GetBuiltInPresetLabel( preset ) );
    }

    private void OpenSavePresetDialog()
    {
        Dialog.AskString(
            SavePreset,
            T( "dialog.save_preset.prompt" ),
            okay: T( "dialog.save_preset.ok" ),
            title: T( "dialog.save_preset.title" ),
            minLength: 1
        );
    }

    private void HandleSavePreset()
    {
        var loadedPreset = GetLoadedCustomPreset();
        if ( loadedPreset == null )
        {
            OpenSavePresetDialog();
            return;
        }

        SaveDraftIntoPreset( loadedPreset, loadedPreset.Name );
    }

    private void SavePreset( string presetName )
    {
        var targetName = ResolveUniquePresetName( presetName );
        if ( string.IsNullOrWhiteSpace( targetName ) )
        {
            return;
        }

        var existing = FindCustomPresetByName( targetName );
        if ( existing == null )
        {
            existing = new UltimateLightManagerCustomPreset();
            _state.CustomPresets.Add( existing );
        }

        SaveDraftIntoPreset( existing, targetName );
    }

    private void OpenDuplicatePresetDialog()
    {
        Dialog.AskString(
            DuplicatePreset,
            T( "dialog.duplicate_preset.prompt" ),
            okay: T( "dialog.duplicate_preset.ok" ),
            title: T( "dialog.duplicate_preset.title" ),
            minLength: 1
        );
    }

    private void DuplicatePreset( string presetName )
    {
        var targetName = ResolveUniquePresetName( presetName );
        if ( string.IsNullOrWhiteSpace( targetName ) )
        {
            return;
        }

        var copy = new UltimateLightManagerCustomPreset();
        _state.CustomPresets.Add( copy );
        SaveDraftIntoPreset( copy, targetName );
    }

    private void OpenRenamePresetDialog()
    {
        if ( GetLoadedCustomPreset() == null )
        {
            return;
        }

        Dialog.AskString(
            RenameLoadedPreset,
            T( "dialog.rename_preset.prompt" ),
            okay: T( "dialog.rename_preset.ok" ),
            title: T( "dialog.rename_preset.title" ),
            minLength: 1
        );
    }

    private void RenameLoadedPreset( string presetName )
    {
        var loadedPreset = GetLoadedCustomPreset();
        if ( loadedPreset == null )
        {
            return;
        }

        var targetName = ResolveUniquePresetName( presetName, loadedPreset );
        if ( string.IsNullOrWhiteSpace( targetName ) )
        {
            return;
        }

        loadedPreset.Name = targetName;
        _state.SelectedPresetKey = UltimateLightManagerEditorActions.GetCustomPresetKey( targetName );
        _state.LoadedPresetKey = _state.SelectedPresetKey;
        UltimateLightManagerEditorActions.SaveState( _state );
        RefreshPresetWorkflowUi();
        BuildUi();
    }

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

        Dialog.AskConfirm(
            () => DeletePreset( selectedPreset ),
            TF( "dialog.delete_preset.message", selectedPreset.Name ),
            T( "dialog.delete_preset.title" ),
            T( "dialog.delete_preset.ok" ),
            T( "dialog.cancel" )
        );
    }

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

    private void LoadFromSelection()
    {
        if ( !UltimateLightManagerEditorActions.LoadDraftFromSelection( _draft ) )
        {
            return;
        }

        _state.SelectedPresetKey = UltimateLightManagerEditorActions.GetBuiltInPresetKey( _draft.SelectedPreset );
        _state.LoadedPresetKey = string.Empty;
        UltimateLightManagerEditorActions.SaveState( _state );
        RefreshPresetWorkflowUi();
        BuildUi();
    }

    private void OpenAssignGroupDialog()
    {
        Dialog.AskString(
            AssignGroupToSelection,
            T( "dialog.assign_group.prompt" ),
            okay: T( "dialog.assign_group.ok" ),
            title: T( "dialog.assign_group.title" ),
            minLength: 1
        );
    }

    private void AssignGroupToSelection( string groupName )
    {
        UltimateLightManagerEditorActions.AssignGroupToSelection( groupName );
    }

    private void OpenAssignPowerGridDialog()
    {
        Dialog.AskString(
            AssignPowerGridToSelection,
            T( "dialog.assign_grid.prompt" ),
            okay: T( "dialog.assign_grid.ok" ),
            title: T( "dialog.assign_grid.title" ),
            minLength: 1
        );
    }

    private void AssignPowerGridToSelection( string powerGridTag )
    {
        UltimateLightManagerEditorActions.AssignPowerGridToSelection( powerGridTag );
    }

    private void RevertToLoadedPreset()
    {
        if ( !UltimateLightManagerEditorActions.TryGetPresetSnapshot( _state, _state.LoadedPresetKey, out var presetData ) )
        {
            return;
        }

        _draft.ApplyPresetData( presetData );
        UltimateLightManagerEditorActions.SaveState( _state );
        RefreshPresetWorkflowUi();
        BuildUi();
    }

    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 UltimateLightManagerCustomPreset GetLoadedCustomPreset()
    {
        if ( string.IsNullOrWhiteSpace( _state.LoadedPresetKey ) || !UltimateLightManagerEditorActions.IsCustomPresetKey( _state.LoadedPresetKey ) )
        {
            return null;
        }

        return _state.CustomPresets.FirstOrDefault( preset =>
            string.Equals( UltimateLightManagerEditorActions.GetCustomPresetKey( preset.Name ), _state.LoadedPresetKey, StringComparison.OrdinalIgnoreCase ) );
    }

    private UltimateLightManagerCustomPreset FindCustomPresetByName( string presetName )
    {
        return _state.CustomPresets.FirstOrDefault( preset => string.Equals( preset.Name, presetName, StringComparison.OrdinalIgnoreCase ) );
    }

    private string ResolveUniquePresetName( string presetName, UltimateLightManagerCustomPreset ignoredPreset = null )
    {
        presetName = presetName?.Trim();
        if ( string.IsNullOrWhiteSpace( presetName ) )
        {
            return string.Empty;
        }

        string candidate = presetName;
        int suffix = 2;

        while ( _state.CustomPresets.Any( preset =>
            !ReferenceEquals( preset, ignoredPreset ) &&
            string.Equals( preset.Name, candidate, StringComparison.OrdinalIgnoreCase ) ) )
        {
            candidate = $"{presetName} {suffix}";
            suffix++;
        }

        return candidate;
    }

    private void SaveDraftIntoPreset( UltimateLightManagerCustomPreset preset, string presetName )
    {
        if ( preset == null )
        {
            return;
        }

        preset.Name = presetName;
        preset.Data = _draft.ToPresetData();
        _state.SelectedPresetKey = UltimateLightManagerEditorActions.GetCustomPresetKey( presetName );
        _state.LoadedPresetKey = _state.SelectedPresetKey;

        UltimateLightManagerEditorActions.SaveState( _state );
        RefreshPresetWorkflowUi();
        BuildUi();
    }

    private UltimateLightManagerPresetData GetCurrentDraftSnapshot()
    {
        return _draft.Clone();
    }

    private bool IsDraftModifiedFromLoadedPreset()
    {
        if ( !UltimateLightManagerEditorActions.TryGetPresetSnapshot( _state, _state.LoadedPresetKey, out var presetData ) )
        {
            return false;
        }

        return !GetCurrentDraftSnapshot().ContentEquals( presetData );
    }

    private string GetPresetStatusText()
    {
        if ( !TryGetLoadedPresetLabel( out var presetLabel ) )
        {
            return T( "status.preset_free" );
        }

        return IsDraftModifiedFromLoadedPreset()
            ? TF( "status.preset_modified_format", presetLabel )
            : TF( "status.preset_clean_format", presetLabel );
    }

    private bool TryGetLoadedPresetLabel( out string presetLabel )
    {
        presetLabel = string.Empty;

        if ( string.IsNullOrWhiteSpace( _state.LoadedPresetKey ) )
        {
            return false;
        }

        if ( UltimateLightManagerEditorActions.TryParseBuiltInPresetKey( _state.LoadedPresetKey, out var builtInPreset ) )
        {
            presetLabel = Texts.GetBuiltInPresetLabel( builtInPreset );
            return true;
        }

        var customPreset = GetLoadedCustomPreset();
        if ( customPreset == null )
        {
            return false;
        }

        presetLabel = Texts.GetCustomPresetLabel( customPreset.Name );
        return true;
    }

    private void RefreshPresetWorkflowUi()
    {
        bool hasLoadedPreset = UltimateLightManagerEditorActions.TryGetPresetSnapshot( _state, _state.LoadedPresetKey, out _ );
        bool isModified = hasLoadedPreset && IsDraftModifiedFromLoadedPreset();
        bool hasLoadedCustomPreset = GetLoadedCustomPreset() != null;

        if ( _presetStatusLabel != null )
        {
            _presetStatusLabel.Text = GetPresetStatusText();
        }

        if ( _renamePresetButton != null )
        {
            _renamePresetButton.Enabled = hasLoadedCustomPreset;
        }

        if ( _deletePresetButton != null )
        {
            _deletePresetButton.Enabled = hasLoadedCustomPreset;
        }

        if ( _revertPresetButton != null )
        {
            _revertPresetButton.Enabled = isModified;
        }

        if ( _duplicatePresetButton != null )
        {
            _duplicatePresetButton.Enabled = true;
        }

        if ( _savePresetButton != null )
        {
            _savePresetButton.Enabled = true;
        }
    }

    private SerializedObject GetSelectionObject( UltimateLightManager[] lights )
    {
        if ( lights == null || lights.Length == 0 )
        {
            return null;
        }

        if ( lights.Length == 1 )
        {
            return lights[0].GetSerialized();
        }

        var multi = new MultiSerializedObject();
        foreach ( var light in lights )
        {
            if ( light == null )
            {
                continue;
            }

            multi.Add( light.GetSerialized() );
        }

        multi.Rebuild();
        return multi;
    }

    private string GetCurrentTransformModeName()
    {
        return _tool.CurrentTool switch
        {
            UltimateLightManagerRotationTool => T( "mode.rotate" ),
            UltimateLightManagerScaleTool => T( "mode.scale" ),
            _ => T( "mode.move" )
        };
    }

    private string GetSelectionSummaryText( int count )
    {
        return count switch
        {
            0 => T( "summary.none_selected" ),
            1 => T( "summary.one_selected" ),
            _ => TF( "summary.many_selected", count )
        };
    }

    private void ToggleHelpTexts()
    {
        _state.ShowHelpTexts = !_state.ShowHelpTexts;
        UltimateLightManagerEditorActions.SaveState( _state );
        BuildUi();
    }

    private void ToggleShortcutSection()
    {
        _state.ShowShortcutSection = !_state.ShowShortcutSection;
        UltimateLightManagerEditorActions.SaveState( _state );
        BuildUi();
    }

    private bool ShouldShowDraftProperty( SerializedProperty property )
    {
        if ( property == null )
        {
            return false;
        }

        if ( property.Name == nameof( UltimateLightManagerPresetData.SelectedPreset ) )
        {
            return false;
        }

        if ( property.Name == nameof( UltimateLightManagerPresetData.AutoApplyPreset ) )
        {
            return false;
        }

        if ( property.Name == nameof( UltimateLightManagerPresetData.ShowDebugGizmos ) )
        {
            return false;
        }

        if ( property.Name == nameof( UltimateLightManagerPresetData.AmbientSound ) ||
             property.Name == nameof( UltimateLightManagerPresetData.ToggleOnSound ) ||
             property.Name == nameof( UltimateLightManagerPresetData.ToggleOffSound ) ||
             property.Name == nameof( UltimateLightManagerPresetData.ModulateVolumeWithLight ) ||
             property.Name == nameof( UltimateLightManagerPresetData.ModulatePitchWithLight ) ||
             property.Name == nameof( UltimateLightManagerPresetData.BaseVolume ) ||
             property.Name == nameof( UltimateLightManagerPresetData.MinPitch ) ||
             property.Name == nameof( UltimateLightManagerPresetData.MaxPitch ) )
        {
            return false;
        }

        if ( property.Name == nameof( UltimateLightManagerPresetData.MaxDistance ) ||
             property.Name == nameof( UltimateLightManagerPresetData.ShadowMaxDistance ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnableCulling ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnableAdaptiveUpdates ) ||
             property.Name == nameof( UltimateLightManagerPresetData.NearUpdateRate ) ||
             property.Name == nameof( UltimateLightManagerPresetData.FarUpdateRate ) )
        {
            return false;
        }

        if ( property.Name == nameof( UltimateLightManagerPresetData.DefaultAlarmDuration ) ||
             property.Name == nameof( UltimateLightManagerPresetData.AlarmColor ) ||
             property.Name == nameof( UltimateLightManagerPresetData.AlarmStrobeSpeed ) ||
             property.Name == nameof( UltimateLightManagerPresetData.AlarmBrightnessMultiplier ) )
        {
            return false;
        }

        if ( property.Name == nameof( UltimateLightManagerPresetData.EnableFire ) ||
             property.Name == nameof( UltimateLightManagerPresetData.FireSpeed ) ||
             property.Name == nameof( UltimateLightManagerPresetData.FireIntensity ) ||
             property.Name == nameof( UltimateLightManagerPresetData.FireChaos ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnableHorror ) ||
             property.Name == nameof( UltimateLightManagerPresetData.MinFlickerDelay ) ||
             property.Name == nameof( UltimateLightManagerPresetData.MaxFlickerDelay ) ||
             property.Name == nameof( UltimateLightManagerPresetData.DamageSeverity ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SparkSound ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnableDisco ) ||
             property.Name == nameof( UltimateLightManagerPresetData.DiscoSpeed ) ||
             property.Name == nameof( UltimateLightManagerPresetData.DiscoSaturation ) ||
             property.Name == nameof( UltimateLightManagerPresetData.DiscoValue ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnableColorTransition ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SecondaryColor ) ||
             property.Name == nameof( UltimateLightManagerPresetData.ColorTransitionSpeed ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnableSensor ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SensorRange ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SensorMinBrightness ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SensorMaxBrightness ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SensorSmoothness ) ||
             property.Name == nameof( UltimateLightManagerPresetData.InvertSensor ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnableSway ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SwaySpeedPitch ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SwayAmountPitch ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SwaySpeedRoll ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SwayAmountRoll ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnablePattern ) ||
             property.Name == nameof( UltimateLightManagerPresetData.Pattern ) ||
             property.Name == nameof( UltimateLightManagerPresetData.PatternSpeed ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnablePulse ) ||
             property.Name == nameof( UltimateLightManagerPresetData.PulseSpeed ) ||
             property.Name == nameof( UltimateLightManagerPresetData.PulseMin ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnableStrobe ) ||
             property.Name == nameof( UltimateLightManagerPresetData.StrobeSpeed ) ||
             property.Name == nameof( UltimateLightManagerPresetData.StrobeDutyCycle ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnableKelvin ) ||
             property.Name == nameof( UltimateLightManagerPresetData.KelvinTemperature ) ||
             property.Name == nameof( UltimateLightManagerPresetData.EnablePowerSurge ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SurgeMinInterval ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SurgeMaxInterval ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SurgeDuration ) ||
             property.Name == nameof( UltimateLightManagerPresetData.SurgeBrightnessMultiplier ) )
        {
            return false;
        }

        return true;
    }

    private void AddOptionalHint( Layout layout, string key, Widget parent, float alpha = 0.7f )
    {
        if ( !_state.ShowHelpTexts )
        {
            return;
        }

        layout.Add( CreateHintLabel( T( key ), parent, alpha ) );
    }

    private string T( string key )
    {
        return Texts[key];
    }

    private string TF( string key, params object[] args )
    {
        return Texts.Format( key, args );
    }

    private static Label CreateHintLabel( string text, Widget parent, float alpha = 0.7f )
    {
        var label = new Label( text, parent )
        {
            WordWrap = true
        };

        label.SetStyles( $"color: {Theme.Text.WithAlpha( alpha ).Hex};" );
        return label;
    }

    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 = UltimateLightManagerEditorActions.GetBuiltInPresetKey( preset ),
                BuiltInPreset = preset
            };
        }

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

internal sealed class UltimateLightManagerPage : ToolSidebarWidget
{
    public UltimateLightManagerPage( Widget parent = null ) : base( parent )
    {
        VerticalSizeMode = SizeMode.CanGrow;
        HorizontalSizeMode = SizeMode.Flexible;
        Layout.Margin = 8;
        Layout.Spacing = 4;
    }

    protected override void OnPaint()
    {
    }
}

internal static class UltimateLightManagerEditorActions
{
    private const string StatePath = "ultimate_light_manager_builder.json";

    public 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 );
        loaded.LoadedPresetKey ??= string.Empty;

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

        if ( IsCustomPresetKey( loaded.LoadedPresetKey ) &&
             !loaded.CustomPresets.Any( preset => string.Equals( GetCustomPresetKey( preset.Name ), loaded.LoadedPresetKey, StringComparison.OrdinalIgnoreCase ) ) )
        {
            loaded.LoadedPresetKey = string.Empty;
        }

        return loaded;
    }

    public static void SaveState( UltimateLightManagerBuilderState state )
    {
        if ( state == null )
        {
            return;
        }

        state.Draft ??= new UltimateLightManagerBuilderDraft();
        state.CustomPresets ??= new List<UltimateLightManagerCustomPreset>();
        global::Editor.FileSystem.ProjectSettings.WriteJson( StatePath, state );
    }

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

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

    public static bool IsCustomPresetKey( string presetKey )
    {
        return !string.IsNullOrWhiteSpace( presetKey ) && presetKey.StartsWith( "custom:", StringComparison.OrdinalIgnoreCase );
    }

    public static bool TryParseBuiltInPresetKey( string presetKey, out UltimateLightManager.LightPreset preset )
    {
        preset = UltimateLightManager.LightPreset.Custom;

        if ( string.IsNullOrWhiteSpace( presetKey ) || !presetKey.StartsWith( "builtin:", StringComparison.OrdinalIgnoreCase ) )
        {
            return false;
        }

        return Enum.TryParse( presetKey["builtin:".Length..], true, out preset );
    }

    public static bool TryGetPresetSnapshot( UltimateLightManagerBuilderState state, string presetKey, out UltimateLightManagerPresetData data )
    {
        data = null;

        if ( state == null || string.IsNullOrWhiteSpace( presetKey ) )
        {
            return false;
        }

        if ( TryParseBuiltInPresetKey( presetKey, out var builtInPreset ) )
        {
            var draft = new UltimateLightManagerBuilderDraft();
            ApplyBuiltInPresetToDraft( draft, builtInPreset );
            data = draft.Clone();
            return true;
        }

        var customPreset = state.CustomPresets?.FirstOrDefault( preset =>
            string.Equals( GetCustomPresetKey( preset.Name ), presetKey, StringComparison.OrdinalIgnoreCase ) );

        if ( customPreset?.Data == null )
        {
            return false;
        }

        data = customPreset.Data.Clone();
        return true;
    }

    public static void ApplyBuiltInPresetToDraft( UltimateLightManagerBuilderDraft draft, UltimateLightManager.LightPreset preset )
    {
        if ( draft == null )
        {
            return;
        }

        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;
        }
    }

    public static void CreateLightFromDraft( UltimateLightManagerBuilderDraft draft, Vector3? worldPosition = null, Rotation? worldRotation = null )
    {
        var session = SceneEditorSession.Active;
        if ( session?.Scene == null || draft == null )
        {
            return;
        }

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

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

            if ( worldPosition.HasValue )
            {
                go.WorldPosition = worldPosition.Value;
            }

            if ( worldRotation.HasValue )
            {
                go.WorldRotation = worldRotation.Value;
            }

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

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

    public static void ApplyDraftToSelection( UltimateLightManagerBuilderDraft draft )
    {
        if ( draft == null )
        {
            return;
        }

        var lights = GetSelectedLightManagers();
        if ( lights.Length == 0 )
        {
            return;
        }

        var session = SceneEditorSession.Active;
        if ( session == null )
        {
            return;
        }

        using var scope = SceneEditorSession.Scope();
        using ( session.UndoScope( "Apply Ultimate Light Draft" ).WithComponentChanges( lights ).Push() )
        {
            foreach ( var light in lights )
            {
                ApplyDraftToLight( draft, light );
            }
        }
    }

    public static bool LoadDraftFromSelection( UltimateLightManagerBuilderDraft draft )
    {
        var selectedLight = GetSelectedLightManagers().FirstOrDefault();
        if ( selectedLight == null || draft == null )
        {
            return false;
        }

        draft.LightName = string.IsNullOrWhiteSpace( selectedLight.GameObject?.Name ) ? draft.LightName : selectedLight.GameObject.Name;
        draft.CaptureFromLight( selectedLight );
        return true;
    }

    public static void AssignGroupToSelection( string groupName )
    {
        groupName = groupName?.Trim();
        if ( string.IsNullOrWhiteSpace( groupName ) )
        {
            return;
        }

        ApplyToSelection( $"Assign Group {groupName}", light => light.LightGroup = groupName );
    }

    public static void AssignPowerGridToSelection( string powerGridTag )
    {
        powerGridTag = powerGridTag?.Trim();
        if ( string.IsNullOrWhiteSpace( powerGridTag ) )
        {
            return;
        }

        ApplyToSelection( $"Assign Power Grid {powerGridTag}", light => light.PowerGridTag = powerGridTag );
    }

    public static void SelectSameGroup()
    {
        var source = GetSelectedLightManagers().FirstOrDefault();
        var session = SceneEditorSession.Active;

        if ( source == null || session?.Scene == null || string.IsNullOrWhiteSpace( source.LightGroup ) )
        {
            return;
        }

        SelectLightsInScene( session, light => string.Equals( light.LightGroup, source.LightGroup, StringComparison.OrdinalIgnoreCase ) );
    }

    public static void SelectSamePowerGrid()
    {
        var source = GetSelectedLightManagers().FirstOrDefault();
        var session = SceneEditorSession.Active;

        if ( source == null || session?.Scene == null || string.IsNullOrWhiteSpace( source.PowerGridTag ) )
        {
            return;
        }

        SelectLightsInScene( session, light => string.Equals( light.PowerGridTag, source.PowerGridTag, StringComparison.OrdinalIgnoreCase ) );
    }

    public static void ApplyToSelection( string undoName, Action<UltimateLightManager> action )
    {
        var session = SceneEditorSession.Active;
        if ( session == null || action == null )
        {
            return;
        }

        var lights = GetSelectedLightManagers();
        if ( lights.Length == 0 )
        {
            return;
        }

        using var scope = SceneEditorSession.Scope();
        using ( session.UndoScope( undoName ).WithComponentChanges( lights ).Push() )
        {
            foreach ( var light in lights )
            {
                action( light );
            }
        }
    }

    public static UltimateLightManager[] GetSelectedLightManagers()
    {
        var lights = new HashSet<UltimateLightManager>();

        foreach ( var light in EditorScene.Selection.OfType<UltimateLightManager>() )
        {
            if ( light != null )
            {
                lights.Add( light );
            }
        }

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

        foreach ( var go in GetSelectedGameObjects() )
        {
            foreach ( var light in go.Components.GetAll<UltimateLightManager>( FindMode.InSelf | FindMode.InDescendants | FindMode.Enabled | FindMode.Disabled ) )
            {
                if ( light != null )
                {
                    lights.Add( light );
                }
            }
        }

        return lights.ToArray();
    }

    public static string GetSelectionSummary( UltimateLightManager[] lights, Func<UltimateLightManager, string> selector )
    {
        if ( lights == null || lights.Length == 0 || selector == null )
        {
            return string.Empty;
        }

        var values = lights
            .Select( selector )
            .Where( x => !string.IsNullOrWhiteSpace( x ) )
            .Distinct( StringComparer.OrdinalIgnoreCase )
            .ToArray();

        return values.Length switch
        {
            0 => string.Empty,
            1 => values[0],
            _ => "plusieurs valeurs"
        };
    }

    public static void ApplyDraftToLight( UltimateLightManagerBuilderDraft draft, UltimateLightManager light )
    {
        if ( draft == null || light == null )
        {
            return;
        }

        draft.ApplyToLight( light );
    }

    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 SelectLightsInScene( SceneEditorSession session, Func<UltimateLightManager, bool> predicate )
    {
        using var scope = SceneEditorSession.Scope();

        var lights = session.Scene
            .GetAllComponents<UltimateLightManager>()
            .Where( predicate )
            .ToArray();

        if ( lights.Length == 0 )
        {
            return;
        }

        EditorScene.Selection.Clear();
        foreach ( var light in lights )
        {
            EditorScene.Selection.Add( light.GameObject );
        }
    }

    private static GameObject GetSelectedParentGameObject()
    {
        return GetSelectedGameObjects().FirstOrDefault();
    }

    private static GameObject[] GetSelectedGameObjects()
    {
        var gameObjects = new HashSet<GameObject>();

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

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

        return gameObjects.ToArray();
    }
}