XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
UTF-8
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Newtonsoft.Json;
using SpaceEngineersBlueprintEditor.Implements.Services;
using SpaceEngineersBlueprintEditor.Interface.Services;
using SpaceEngineersBlueprintEditor.Model;
using SpaceEngineersBlueprintEditor.SpaceEngineersCore;
using SpaceEngineersBlueprintEditor.SpaceEngineersCore.BlueprintEditing;
using SpaceEngineersBlueprintEditor.SpaceEngineersCore.GridConverter;
using SpaceEngineersBlueprintEditor.Utilities;
using SpaceEngineersBlueprintEditor.Views;
using System.Collections.ObjectModel;
using System.Diagnostics;
using VRage;
using VRage.Game;
using VRage.ObjectBuilders;
using VRageMath;
using Windows.Storage;
using Windows.Storage.Pickers;
using WinRT.Interop;
using XFEExtension.NetCore.WinUIHelper.Interface.Services;
using XFEExtension.NetCore.WinUIHelper.Utilities;
using XFEExtension.NetCore.WinUIHelper.Utilities.Helper;

namespace SpaceEngineersBlueprintEditor.ViewModels;

public partial class BlueprintEditSubPageViewModel : ViewModelBase
{
    [ObservableProperty]
    private bool isCubeGridListVisible;
    [ObservableProperty]
    private bool isShipBlueprintPropertyVisible;
    [ObservableProperty]
    private bool isThreeDimensionalEditorVisible;
    [ObservableProperty]
    private bool isSheetMetalEditorVisible;
    [ObservableProperty]
    private bool isContentGridVisible = false;
    [ObservableProperty]
    private bool isInitialGridVisible = true;
    [ObservableProperty]
    private string searchText = string.Empty;
    [ObservableProperty]
    private string propertiesSearchText = string.Empty;
    [ObservableProperty]
    private BlueprintPropertyViewData? selectedCubeBlock;
    [ObservableProperty]
    private object? selectedCubeBlockObject;
    [ObservableProperty]
    private object? selectedCubeGrid;
    [ObservableProperty]
    private TreeViewNode? selectedTreeViewNode;
    [ObservableProperty]
    private string selectedThreeDimensionalBlockText = "No block selected";
    [ObservableProperty]
    private bool hasUnsavedChanges;
    private bool isLoaded;
    private int sceneGeneration;
    private BlueprintModel? currentBlueprintModel;
    private MyObjectBuilder_Definitions? currentDefinitions;
    private MyObjectBuilder_ShipBlueprintDefinition? currentShipBlueprint;
    private IReadOnlyDictionary<int, Blueprint3DBlockReference> sceneBlockReferences =
        new Dictionary<int, Blueprint3DBlockReference>();
    private Blueprint3DBlockReference? selectedSceneBlock;
    private readonly Stack<BlueprintEditAction> undoActions = new();
    private readonly Stack<BlueprintEditAction> redoActions = new();
    private readonly List<BlueprintPropertyViewData> blueprintPropertyRoots = [];
    private readonly List<BlueprintPropertyViewData> cubePropertyRoots = [];
    private readonly ILoadingService? loadingService = ServiceManager.GetGlobalService<ILoadingService>();
    private readonly IMessageService? messageService = ServiceManager.GetGlobalService<IMessageService>();
    private readonly INavigationViewService? navigationViewService = ServiceManager.GetGlobalService<INavigationViewService>();
    private readonly ITabViewTitleService? tabViewTitleService = ServiceManager.GetGlobalService<ITabViewTitleService>();
    public ObservableCollection<BlueprintGroupList> BlueprintCubeGridList { get; } = [];
    public IBackgroundImageService? BackgroundImageService { get; set; } = ServiceManager.GetGlobalService<IBackgroundImageService>();
    public IListViewDisplayService<BlueprintPropertyViewData> ListViewDisplayService { get; set; } = new ListViewDisplayService<BlueprintPropertyViewData>();
    public IAutoNavigationParameterService<BlueprintModel> AutoNavigationParameterService { get; set; } = ServiceManager.GetService<IAutoNavigationParameterService<BlueprintModel>>();
    public IFileDropService FileDropService { get; set; } = new BlueprintDropService();
    public ITreeViewService BlueprintTreeViewService { get; set; } = new TreeViewService();
    public ITreeViewService CubeBlockTreeViewService { get; set; } = new TreeViewService();
    public ISelectorBarService SelectorBarService { get; set; } = ServiceManager.GetService<ISelectorBarService>();

    public event EventHandler<string>? ThreeDimensionalSceneChanged;

    public BlueprintEditSubPageViewModel()
    {
        AutoNavigationParameterService.ParameterChange += AutoNavigationParameterService_ParameterChange;
        ListViewDisplayService.SelectionChanged += ListViewDisplayService_SelectionChanged;
        SelectorBarService.SelectionChanged += SelectorBarService_SelectionChanged;
        FileDropService.Drop += FileDropService_Drop;
    }

    private void ListViewDisplayService_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (e.AddedItems.FirstOrDefault() is BlueprintPropertyViewData blueprintPropertyViewData)
        {
            SelectedCubeBlock = blueprintPropertyViewData;
        }
    }

    partial void OnSelectedCubeBlockChanged(
        BlueprintPropertyViewData? oldValue,
        BlueprintPropertyViewData? newValue)
    {
        var selectedBlock = newValue?.Value as MyObjectBuilder_CubeBlock;
        SelectedCubeBlockObject = selectedBlock;
        SelectedCubeGrid = selectedBlock is null
            ? null
            : currentShipBlueprint?.CubeGrids.FirstOrDefault(grid =>
                grid.CubeBlocks.Any(block => ReferenceEquals(block, selectedBlock)));
        LoadCubeProperties();
    }

    private async void FileDropService_Drop(object? sender, (string, DragEventArgs) e)
    {
        loadingService?.StartLoading<BlueprintEditSubPage>($"{"LoadingDefinitions".GetLocalized()}...");
        if (File.Exists(e.Item1) && await SpaceEngineersHelper.LoadBlueprintModel(e.Item1) is BlueprintModel blueprintModel)
        {
            AutoNavigationParameterService.Parameter = blueprintModel;
            if (blueprintModel.ViewData is not null)
                tabViewTitleService?.SetTabViewTitle(blueprintModel.ViewData.Name, blueprintModel);
            await SetDefinitions(blueprintModel.BlueprintDefinitions);
        }
        loadingService?.StopLoading<BlueprintEditSubPage>();
    }

    partial void OnSearchTextChanged(string value) => SearchCubeGrids(value);

    partial void OnPropertiesSearchTextChanged(string value)
    {
        PopulatePropertyTree(BlueprintTreeViewService, blueprintPropertyRoots, value);
        PopulatePropertyTree(CubeBlockTreeViewService, cubePropertyRoots, value);
    }

    private async void SelectorBarService_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args) => await LoadByName(sender.SelectedItem.Tag as string ?? "Grids");

    private async Task SetDefinitions(MyObjectBuilder_Definitions? definitions)
    {
        if (definitions is not null)
        {
            currentDefinitions = definitions;
            selectedSceneBlock = null;
            sceneBlockReferences = new Dictionary<int, Blueprint3DBlockReference>();
            undoActions.Clear();
            redoActions.Clear();
            HasUnsavedChanges = false;
            if (currentDefinitions.ShipBlueprints is not null && currentDefinitions.ShipBlueprints.Length > 0)
            {
                currentShipBlueprint = currentDefinitions.ShipBlueprints[0];
                IsContentGridVisible = true;
                IsInitialGridVisible = false;
                await Task.Delay(100);
                if (!isLoaded)
                    await LoadByName("Grids");
            }
            else
            {
                messageService?.ShowMessage("Error_CantLoadingShipDefinitions".GetLocalized(), "Error".GetLocalized(), InfoBarSeverity.Error);
            }
        }
        else
        {
            messageService?.ShowMessage("Error_CantFindShipDefinitions".GetLocalized(), "Error".GetLocalized(), InfoBarSeverity.Error);
        }
    }

    private async void AutoNavigationParameterService_ParameterChange(object? sender, BlueprintModel? e)
    {
        if (e is null || AutoNavigationParameterService.SameAsLast)
            return;
        currentBlueprintModel = e;
        if (currentBlueprintModel.ViewData is not null)
            BackgroundImageService?.SetBackgroundImage(currentBlueprintModel.ViewData.BlueprintImage);
        if (navigationViewService is not null) navigationViewService.Header = null;
        if (e.ViewData is not null)
            tabViewTitleService?.SetTabViewTitle(e.ViewData.Name, e);
        if (e.BlueprintDefinitions is not null)
        {
            await SetDefinitions(e.BlueprintDefinitions);
        }
        else if (e.ViewData is not null)
        {
            await SetDefinitions(await SpaceEngineersHelper.LoadBlueprintAsync(e.ViewData.FilePath));
        }
    }

    private async Task LoadByName(string caseName)
    {
        if (currentShipBlueprint is null) return;
        isLoaded = true;
        loadingService?.StartLoading<BlueprintEditSubPage>($"{"LoadingDefinitions".GetLocalized()}...");
        await Task.Delay(50);
        switch (caseName)
        {
            case "Grids":
                IsCubeGridListVisible = true;
                IsShipBlueprintPropertyVisible = false;
                IsThreeDimensionalEditorVisible = false;
                IsSheetMetalEditorVisible = false;
                LoadCubeGrids();
                break;
            case "Groups":
                IsCubeGridListVisible = true;
                IsShipBlueprintPropertyVisible = false;
                IsThreeDimensionalEditorVisible = false;
                IsSheetMetalEditorVisible = false;
                LoadCubeGroups();
                break;
            case "Properties":
                IsCubeGridListVisible = false;
                IsShipBlueprintPropertyVisible = true;
                IsThreeDimensionalEditorVisible = false;
                IsSheetMetalEditorVisible = false;
                LoadBlueprintPropertyDefinitions();
                break;
            case "ThreeDimensional":
                IsCubeGridListVisible = false;
                IsShipBlueprintPropertyVisible = false;
                IsThreeDimensionalEditorVisible = true;
                IsSheetMetalEditorVisible = false;
                await RefreshThreeDimensionalSceneAsync();
                break;
            case "SheetMetal":
                IsCubeGridListVisible = false;
                IsShipBlueprintPropertyVisible = false;
                IsThreeDimensionalEditorVisible = false;
                IsSheetMetalEditorVisible = true;
                LoadCubeGrids();
                break;
            default:
                break;
        }
        loadingService?.StopLoading<BlueprintEditSubPage>();
    }

    private void LoadBlueprintPropertyDefinitions()
    {
        var parent = new BlueprintPropertyViewData
        {
            Value = currentShipBlueprint,
            Name = "Ship blueprint",
            Type = currentShipBlueprint!.GetType(),
            ValueChanged = () => HasUnsavedChanges = true
        };
        SpaceEngineersHelper.AnalyzeBlueprint(parent);
        blueprintPropertyRoots.Clear();
        blueprintPropertyRoots.AddRange(parent.Children);
        PopulatePropertyTree(BlueprintTreeViewService, blueprintPropertyRoots, PropertiesSearchText);
    }

    private void SearchCubeGrids(string name)
    {
        BlueprintCubeGridList.Clear();
        currentShipBlueprint!.CubeGrids.Select(grid =>
        {
            var type = grid.CubeBlocks.GetType();
            var gridViewData = new BlueprintPropertyViewData
            {
                Name = type.Name,
                Type = type,
                Value = grid.CubeBlocks
            };
            SpaceEngineersHelper.AnalyzeBlueprint(gridViewData);
            var targetChild = gridViewData.Children.Where(child => child.MatchesSearch(name));
            if (!targetChild.Any())
                return null;
            return new BlueprintGroupList(targetChild)
            {
                GroupName = grid.DisplayName
            };
        }).Where(grid => grid is not null).ForEach(BlueprintCubeGridList.Add!);
    }

    private async void LoadCubeGrids()
    {
        BlueprintCubeGridList.Clear();
        currentShipBlueprint!.CubeGrids.Select(grid =>
        {
            var type = grid.CubeBlocks.GetType();
            var gridViewData = new BlueprintPropertyViewData
            {
                Name = type.Name,
                Type = type,
                Value = grid.CubeBlocks
            };
            SpaceEngineersHelper.AnalyzeBlueprint(gridViewData);
            return new BlueprintGroupList(gridViewData.Children)
            {
                GroupName = grid.DisplayName
            };
        }).ForEach(BlueprintCubeGridList.Add);
        await Helper.Wait(() => ListViewDisplayService.IsPageLoaded);
        ListViewDisplayService.Select(BlueprintCubeGridList.FirstOrDefault()?.FirstOrDefault());
    }

    private async void LoadCubeGroups()
    {
        BlueprintCubeGridList.Clear();
        var blueprintCubeGridList = new List<BlueprintGroupList>();
        foreach (var grid in currentShipBlueprint!.CubeGrids ?? [])
        {
            if (grid?.CubeBlocks is null)
            {
                continue;
            }

            var type = grid.CubeBlocks.GetType();
            var resolvedGroups = BlockGroupResolver.ResolveGroups(grid);
            foreach (var group in grid.BlockGroups ?? [])
            {
                if (group is null)
                {
                    continue;
                }

                var targetCubeBlocks = resolvedGroups.TryGetValue(group, out var blocks)
                    ? blocks
                    : Array.Empty<MyObjectBuilder_CubeBlock>();
                var groupData = new BlueprintPropertyViewData
                {
                    Name = type.Name,
                    Type = type,
                    Value = targetCubeBlocks
                };
                SpaceEngineersHelper.AnalyzeBlueprint(groupData);
                if (blueprintCubeGridList.FirstOrDefault(item => item.GroupName == group.Name) is BlueprintGroupList blueprintGroupList)
                {
                    blueprintGroupList.AddRange(groupData.Children);
                }
                else
                {
                    blueprintCubeGridList.Add(new BlueprintGroupList(groupData.Children)
                    {
                        GroupName = group.Name ?? string.Empty
                    });
                }
            }
        }
        blueprintCubeGridList.ForEach(BlueprintCubeGridList.Add);
        await Helper.Wait(() => ListViewDisplayService.IsPageLoaded);
        ListViewDisplayService.Select(BlueprintCubeGridList.FirstOrDefault()?.FirstOrDefault());
    }

    private void LoadCubeProperties()
    {
        cubePropertyRoots.Clear();
        if (SelectedCubeBlock is not null)
        {
            SelectedCubeBlock.ValueChanged = () => HasUnsavedChanges = true;
            SpaceEngineersHelper.AnalyzeBlueprint(SelectedCubeBlock);
            SelectedCubeBlock.Children.ForEach(child => child.ValueChanged = SelectedCubeBlock.ValueChanged);
            cubePropertyRoots.AddRange(SelectedCubeBlock.Children);
        }
        PopulatePropertyTree(CubeBlockTreeViewService, cubePropertyRoots, PropertiesSearchText);
    }

    private static void PopulatePropertyTree(
        ITreeViewService treeViewService,
        IEnumerable<BlueprintPropertyViewData> source,
        string searchText)
    {
        treeViewService.Clear();
        foreach (var property in source.Where(property => property.MatchesSearch(searchText)))
        {
            treeViewService.Add(new TreeViewNode
            {
                Content = property,
                HasUnrealizedChildren = property.CanExpand
            });
        }
    }

    [RelayCommand]
    async Task OpenBlueprint()
    {
        loadingService?.StartLoading<BlueprintEditSubPage>($"{"LoadingDefinitions".GetLocalized()}...");
        var openPicker = new FileOpenPicker();
        InitializeWithWindow.Initialize(openPicker, WindowNative.GetWindowHandle(App.MainWindow));
        openPicker.ViewMode = PickerViewMode.List;
        openPicker.FileTypeFilter.Add(".sbc");
        if (await openPicker.PickSingleFileAsync() is StorageFile file && File.Exists(file.Path))
        {
            if (await SpaceEngineersHelper.LoadBlueprintModel(file.Path) is BlueprintModel blueprintModel)
            {
                AutoNavigationParameterService.Parameter = blueprintModel;
                if (blueprintModel.ViewData is not null)
                    tabViewTitleService?.SetTabViewTitle(blueprintModel.ViewData.Name, blueprintModel);
                await SetDefinitions(blueprintModel.BlueprintDefinitions);
            }
        }
        loadingService?.StopLoading<BlueprintEditSubPage>();
    }

    [RelayCommand]
    void OpenInFolder()
    {
        if (currentBlueprintModel is not null && currentBlueprintModel.ViewData is not null)
            Process.Start("explorer.exe", Path.GetDirectoryName(currentBlueprintModel.ViewData.FilePath) ?? string.Empty);
        else
            messageService?.ShowMessage("Warning_CantFindFile".GetLocalized(), "Warning".GetLocalized(), InfoBarSeverity.Warning);
    }

    [RelayCommand]
    void ViewBlueprintsList() => navigationViewService?.NavigateTo<BlueprintsViewPage>("Local");

    [RelayCommand]
    async Task Save()
    {
        var path = currentBlueprintModel?.ViewData?.FilePath;
        if (string.IsNullOrWhiteSpace(path))
        {
            await SaveAs();
            return;
        }

        await SaveToPath(path);
    }

    [RelayCommand]
    async Task SaveAs()
    {
        if (currentDefinitions is null)
        {
            return;
        }

        var savePicker = new FileSavePicker();
        InitializeWithWindow.Initialize(savePicker, WindowNative.GetWindowHandle(App.MainWindow));
        savePicker.FileTypeChoices.Add(new("BlueprintFile".GetLocalized(), [".sbc"]));
        if (currentBlueprintModel?.ViewData?.FilePath is string currentPath && File.Exists(currentPath))
        {
            savePicker.SuggestedSaveFile = await StorageFile.GetFileFromPathAsync(currentPath);
        }
        savePicker.SuggestedFileName = "bp.sbc";
        savePicker.DefaultFileExtension = ".sbc";
        if (await savePicker.PickSaveFileAsync() is StorageFile file)
        {
            await SaveToPath(file.Path);
            if (currentBlueprintModel is not null)
            {
                currentBlueprintModel.ViewData = SpaceEngineersHelper.LoadBlueprintInfo(file.Path)?.ToBlueprintInfoViewData();
            }
        }
    }

    private async Task SaveToPath(string path)
    {
        if (currentDefinitions is null)
        {
            return;
        }

        loadingService?.StartLoading<BlueprintEditSubPage>($"{"SavingBlueprint".GetLocalized()}...");
        var cachePath = Path.Combine(AppPathHelper.AppCache, $"blueprint-{Guid.NewGuid():N}.json");
        try
        {
            Directory.CreateDirectory(AppPathHelper.AppCache);
            await File.WriteAllTextAsync(cachePath, JsonConvert.SerializeObject(
                currentDefinitions,
                new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }));

            var converterPath = GetBlueprintConverterPath();
            using var process = new Process
            {
                StartInfo = new ProcessStartInfo
                {
                    FileName = converterPath,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    CreateNoWindow = true
                }
            };
            process.StartInfo.ArgumentList.Add("--input");
            process.StartInfo.ArgumentList.Add(cachePath);
            process.StartInfo.ArgumentList.Add("--output");
            process.StartInfo.ArgumentList.Add(path);
            process.Start();
            var standardOutput = process.StandardOutput.ReadToEndAsync();
            var standardError = process.StandardError.ReadToEndAsync();
            await process.WaitForExitAsync();
            var result = await standardOutput;
            var error = await standardError;
            if (process.ExitCode != 0 || !string.Equals(result, "Successful", StringComparison.Ordinal))
            {
                throw new InvalidOperationException(
                    string.IsNullOrWhiteSpace(result) ? error : result.Replace("Error:", string.Empty));
            }

            HasUnsavedChanges = false;
            messageService?.ShowMessage("SavingSuccessful".GetLocalized(), "Complete".GetLocalized(), InfoBarSeverity.Success);
        }
        catch (Exception ex)
        {
            messageService?.ShowMessage($"{"Failed_SaveFailed".GetLocalized()}: {ex.Message}", "Failed".GetLocalized(), InfoBarSeverity.Error);
        }
        finally
        {
            if (File.Exists(cachePath))
            {
                File.Delete(cachePath);
            }
            loadingService?.StopLoading<BlueprintEditSubPage>();
        }
    }

    private static string GetBlueprintConverterPath()
    {
        var deployedPath = Path.Combine(
            AppContext.BaseDirectory,
            "Converter",
            "SpaceEngineersBlueprintEditor.BlueprintConverter.exe");
        if (File.Exists(deployedPath))
        {
            return deployedPath;
        }

        var parent = new DirectoryInfo(AppContext.BaseDirectory);
        while (parent is not null)
        {
            foreach (var configuration in new[] { "Debug", "Release" })
            {
                var developmentPath = Path.Combine(
                    parent.FullName,
                    "SpaceEngineersBlueprintEditor.BlueprintConverter",
                    "bin", configuration, "net48",
                    "SpaceEngineersBlueprintEditor.BlueprintConverter.exe");
                if (File.Exists(developmentPath))
                {
                    return developmentPath;
                }
            }
            parent = parent.Parent;
        }

        throw new FileNotFoundException("Blueprint converter was not deployed with the application.", deployedPath);
    }

    [RelayCommand]
    async Task ConvertBlueprint(string command)
    {
        if (currentShipBlueprint is null)
        {
            return;
        }

        BlueprintTransformResult? transformResult = null;
        var changed = false;
        if (currentShipBlueprint is not null)
            switch (command)
            {
                case "Destructible":
                    foreach (var grid in currentShipBlueprint.CubeGrids)
                        grid.DestructibleBlocks = true;
                    changed = true;
                    break;
                case "Indestructible":
                    foreach (var grid in currentShipBlueprint.CubeGrids)
                        grid.DestructibleBlocks = false;
                    changed = true;
                    break;
                case "Editable":
                    foreach (var grid in currentShipBlueprint.CubeGrids)
                        grid.Editable = true;
                    changed = true;
                    break;
                case "Non-Editable":
                    foreach (var grid in currentShipBlueprint.CubeGrids)
                        grid.Editable = false;
                    changed = true;
                    break;
                case "SmallGrid":
                    transformResult = BlueprintTransformService.ConvertGridSize(currentShipBlueprint, MyCubeSize.Small);
                    break;
                case "LargeGrid":
                    transformResult = BlueprintTransformService.ConvertGridSize(currentShipBlueprint, MyCubeSize.Large);
                    break;
                case "LightBlock":
                    transformResult = BlueprintTransformService.ConvertArmor(currentShipBlueprint, BlueprintArmorType.Light);
                    break;
                case "HeavyBlock":
                    transformResult = BlueprintTransformService.ConvertArmor(currentShipBlueprint, BlueprintArmorType.Heavy);
                    break;
                case "Static":
                    foreach (var grid in currentShipBlueprint.CubeGrids)
                        grid.IsStatic = true;
                    changed = true;
                    break;
                case "Active":
                    foreach (var grid in currentShipBlueprint.CubeGrids)
                        grid.IsStatic = false;
                    changed = true;
                    break;
                default:
                    break;
            }

        if (transformResult is not null)
        {
            changed = transformResult.ConvertedBlocks > 0 || transformResult.ChangedGrids > 0;
            var summary = string.Format(
                "BlueprintTransformSummary".GetLocalized(),
                transformResult.ConvertedBlocks,
                transformResult.AlreadyTargetBlocks,
                transformResult.UnsupportedBlocks);
            messageService?.ShowMessage(summary, "Complete".GetLocalized(), InfoBarSeverity.Success);
        }
        else
        {
            messageService?.ShowMessage("ConvertComplete".GetLocalized(), "Complete".GetLocalized(), InfoBarSeverity.Success);
        }

        if (changed)
        {
            HasUnsavedChanges = true;
            await RefreshEditorViewsAsync();
        }
    }

    public void SelectThreeDimensionalBlock(int id)
    {
        if (!sceneBlockReferences.TryGetValue(id, out var reference))
        {
            selectedSceneBlock = null;
            SelectedThreeDimensionalBlockText = "Blueprint3D_NoSelection".GetLocalized();
            return;
        }

        selectedSceneBlock = reference;
        var block = reference.Block;
        var gridName = string.IsNullOrWhiteSpace(reference.Grid.DisplayName)
            ? $"Grid {reference.GridIndex + 1}"
            : reference.Grid.DisplayName;
        SelectedThreeDimensionalBlockText =
            $"{gridName} · {block.SubtypeName} · ({block.Min.X}, {block.Min.Y}, {block.Min.Z})";
    }

    public async Task HandleThreeDimensionalEditorActionAsync(string action)
    {
        switch (action)
        {
            case "moveXPositive":
                await MoveSelectedBlockAsync(1, 0, 0);
                break;
            case "moveXNegative":
                await MoveSelectedBlockAsync(-1, 0, 0);
                break;
            case "moveYPositive":
                await MoveSelectedBlockAsync(0, 1, 0);
                break;
            case "moveYNegative":
                await MoveSelectedBlockAsync(0, -1, 0);
                break;
            case "moveZPositive":
                await MoveSelectedBlockAsync(0, 0, 1);
                break;
            case "moveZNegative":
                await MoveSelectedBlockAsync(0, 0, -1);
                break;
            case "rotateX":
                await RotateSelectedBlockAsync('X');
                break;
            case "rotateY":
                await RotateSelectedBlockAsync('Y');
                break;
            case "rotateZ":
                await RotateSelectedBlockAsync('Z');
                break;
            case "delete":
                await DeleteSelectedBlockAsync();
                break;
            case "duplicate":
                await DuplicateSelectedBlockAsync();
                break;
            case "undo":
                await UndoThreeDimensionalEditAsync();
                break;
            case "redo":
                await RedoThreeDimensionalEditAsync();
                break;
            case "save":
                await Save();
                break;
            case "saveAs":
                await SaveAs();
                break;
            case "refresh":
                await RefreshThreeDimensionalSceneAsync();
                break;
        }
    }

    public async Task RefreshThreeDimensionalSceneAsync()
    {
        if (currentShipBlueprint is null)
        {
            return;
        }

        var generation = ++sceneGeneration;
        var selectedBlock = selectedSceneBlock?.Block;
        // Build from the live object-builder graph on the UI thread. WebView messages can
        // otherwise mutate CubeBlocks while a background enumeration is still in progress.
        var buildResult = Blueprint3DSceneBuilder.Build(currentShipBlueprint);
        if (generation != sceneGeneration)
        {
            return;
        }

        int? selectedId = null;
        if (selectedBlock is not null)
        {
            foreach (var pair in buildResult.BlockReferences)
            {
                if (ReferenceEquals(pair.Value.Block, selectedBlock))
                {
                    selectedId = pair.Key;
                    selectedSceneBlock = pair.Value;
                    break;
                }
            }
        }

        buildResult.Scene.SelectedBlockId = selectedId;
        sceneBlockReferences = buildResult.BlockReferences;
        var sceneJson = await Task.Run(() => JsonConvert.SerializeObject(buildResult.Scene));
        if (generation == sceneGeneration)
        {
            ThreeDimensionalSceneChanged?.Invoke(this, sceneJson);
        }
    }

    private async Task RefreshEditorViewsAsync()
    {
        if (IsThreeDimensionalEditorVisible)
        {
            await RefreshThreeDimensionalSceneAsync();
        }
        else if (IsCubeGridListVisible)
        {
            LoadCubeGrids();
        }
        else if (IsShipBlueprintPropertyVisible)
        {
            LoadBlueprintPropertyDefinitions();
        }
    }

    private async Task MoveSelectedBlockAsync(int x, int y, int z)
    {
        if (selectedSceneBlock is not { } reference)
        {
            return;
        }

        var oldMinimum = reference.Block.Min;
        var newMinimum = new SerializableVector3I(
            oldMinimum.X + x,
            oldMinimum.Y + y,
            oldMinimum.Z + z);
        if (reference.Grid.CubeBlocks.Any(block =>
                !ReferenceEquals(block, reference.Block) && SameMinimum(block.Min, newMinimum)))
        {
            messageService?.ShowMessage(
                "Blueprint3D_PositionOccupied".GetLocalized(),
                "Warning".GetLocalized(),
                InfoBarSeverity.Warning);
            return;
        }

        await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
            () => SetBlockMinimum(reference, newMinimum),
            () => SetBlockMinimum(reference, oldMinimum)));
    }

    private async Task RotateSelectedBlockAsync(char axis)
    {
        if (selectedSceneBlock is not { } reference)
        {
            return;
        }

        var oldOrientation = reference.Block.BlockOrientation;
        var newOrientation = new SerializableBlockOrientation(
            RotateDirection(oldOrientation.Forward, axis),
            RotateDirection(oldOrientation.Up, axis));
        await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
            () => reference.Block.BlockOrientation = newOrientation,
            () => reference.Block.BlockOrientation = oldOrientation));
    }

    private async Task DeleteSelectedBlockAsync()
    {
        if (selectedSceneBlock is not { } reference)
        {
            return;
        }

        var blocks = reference.Grid.CubeBlocks;
        var originalIndex = blocks.IndexOf(reference.Block);
        if (originalIndex < 0)
        {
            return;
        }

        var oldMinimum = new Vector3I(reference.Block.Min.X, reference.Block.Min.Y, reference.Block.Min.Z);
        var groups = (reference.Grid.BlockGroups ?? [])
            .Where(group => group.Blocks?.Any(position => position == oldMinimum) == true)
            .ToArray();
        await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
            () =>
            {
                blocks.Remove(reference.Block);
                foreach (var group in groups)
                {
                    group.Blocks.RemoveAll(position => position == oldMinimum);
                }
                selectedSceneBlock = null;
                SelectedThreeDimensionalBlockText = "Blueprint3D_NoSelection".GetLocalized();
            },
            () =>
            {
                blocks.Insert(Math.Min(originalIndex, blocks.Count), reference.Block);
                foreach (var group in groups)
                {
                    if (!group.Blocks.Contains(oldMinimum))
                    {
                        group.Blocks.Add(oldMinimum);
                    }
                }
                selectedSceneBlock = reference;
            }));
    }

    private async Task DuplicateSelectedBlockAsync()
    {
        if (selectedSceneBlock is not { } reference)
        {
            return;
        }

        var clone = (MyObjectBuilder_CubeBlock)MyObjectBuilderSerializer.Clone(reference.Block);
        clone.EntityId = 0;
        var offset = 1;
        while (offset < 1024 && reference.Grid.CubeBlocks.Any(block =>
                   block.Min.X == reference.Block.Min.X + offset &&
                   block.Min.Y == reference.Block.Min.Y &&
                   block.Min.Z == reference.Block.Min.Z))
        {
            offset++;
        }
        clone.Min = new SerializableVector3I(
            reference.Block.Min.X + offset,
            reference.Block.Min.Y,
            reference.Block.Min.Z);
        var cloneReference = new Blueprint3DBlockReference
        {
            Grid = reference.Grid,
            Block = clone,
            GridIndex = reference.GridIndex
        };
        await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
            () =>
            {
                reference.Grid.CubeBlocks.Add(clone);
                selectedSceneBlock = cloneReference;
            },
            () =>
            {
                reference.Grid.CubeBlocks.Remove(clone);
                selectedSceneBlock = reference;
            }));
    }

    private async Task ExecuteThreeDimensionalEditAsync(BlueprintEditAction editAction)
    {
        editAction.Apply();
        undoActions.Push(editAction);
        redoActions.Clear();
        HasUnsavedChanges = true;
        await RefreshThreeDimensionalSceneAsync();
    }

    private async Task UndoThreeDimensionalEditAsync()
    {
        if (!undoActions.TryPop(out var action))
        {
            return;
        }
        action.Undo();
        redoActions.Push(action);
        HasUnsavedChanges = true;
        await RefreshThreeDimensionalSceneAsync();
    }

    private async Task RedoThreeDimensionalEditAsync()
    {
        if (!redoActions.TryPop(out var action))
        {
            return;
        }
        action.Apply();
        undoActions.Push(action);
        HasUnsavedChanges = true;
        await RefreshThreeDimensionalSceneAsync();
    }

    private static void SetBlockMinimum(Blueprint3DBlockReference reference, SerializableVector3I minimum)
    {
        var oldMinimum = new Vector3I(reference.Block.Min.X, reference.Block.Min.Y, reference.Block.Min.Z);
        reference.Block.Min = minimum;
        var newMinimum = new Vector3I(minimum.X, minimum.Y, minimum.Z);
        foreach (var group in reference.Grid.BlockGroups ?? [])
        {
            for (var index = 0; index < group.Blocks.Count; index++)
            {
                if (group.Blocks[index] == oldMinimum)
                {
                    group.Blocks[index] = newMinimum;
                }
            }
        }
    }

    private static bool SameMinimum(SerializableVector3I left, SerializableVector3I right)
    {
        return left.X == right.X && left.Y == right.Y && left.Z == right.Z;
    }

    private static Base6Directions.Direction RotateDirection(Base6Directions.Direction direction, char axis)
    {
        var vector = direction switch
        {
            Base6Directions.Direction.Forward => new Vector3I(0, 0, -1),
            Base6Directions.Direction.Backward => new Vector3I(0, 0, 1),
            Base6Directions.Direction.Left => new Vector3I(-1, 0, 0),
            Base6Directions.Direction.Right => new Vector3I(1, 0, 0),
            Base6Directions.Direction.Up => new Vector3I(0, 1, 0),
            Base6Directions.Direction.Down => new Vector3I(0, -1, 0),
            _ => new Vector3I(0, 0, -1)
        };
        var rotated = axis switch
        {
            'X' => new Vector3I(vector.X, -vector.Z, vector.Y),
            'Y' => new Vector3I(vector.Z, vector.Y, -vector.X),
            'Z' => new Vector3I(-vector.Y, vector.X, vector.Z),
            _ => vector
        };
        return Base6Directions.GetDirection(rotated);
    }

    private sealed class BlueprintEditAction(Action apply, Action undo)
    {
        public Action Apply { get; } = apply;

        public Action Undo { get; } = undo;
    }

    [RelayCommand]
    void ConvertToSecBlueprint()
    {
        var exportHelper = new GridExportHelper();
        //exportHelper.ExportGrid(currentShipBlueprint?.CubeGrids.First());
    }
}