package p.studio.workspaces.assets.details;

import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.apache.commons.collections4.CollectionUtils;
import p.packer.dtos.PackerAssetActionAvailabilityDTO;
import p.packer.dtos.PackerAssetDetailsDTO;
import p.packer.dtos.PackerDiagnosticDTO;
import p.packer.messages.*;
import p.packer.messages.assets.AssetFamilyCatalog;
import p.studio.Container;
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
import p.studio.controls.forms.StudioSection;
import p.studio.lsp.events.StudioWorkspaceEventBus;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionControl;
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
import p.studio.workspaces.assets.details.palette.AssetDetailsPaletteOverhaulingControl;
import p.studio.workspaces.assets.details.scene.AssetDetailsSceneLayersControl;
import p.studio.workspaces.assets.details.schema.AssetDetailsColorSchemaGroupsControl;
import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl;
import p.studio.workspaces.assets.dialogs.AssetDiagnosticsDialog;
import p.studio.workspaces.assets.messages.*;
import p.studio.workspaces.assets.messages.events.*;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
import p.studio.workspaces.assets.metadata.AssetStudioMetadataSnapshot;
import p.studio.workspaces.assets.scene.SceneBankWorkflowResult;
import p.studio.workspaces.assets.scene.SceneBankWorkflowService;
import p.studio.workspaces.assets.tiled.TiledAssetGenerationResult;
import p.studio.workspaces.assets.tiled.TiledAssetGenerationService;
import p.studio.workspaces.assets.tiled.TiledAssetGenerationService.SceneTmxExportDisposition;
import p.studio.workspaces.assets.wizards.DeleteAssetDialog;
import p.studio.workspaces.assets.wizards.MoveAssetWizard;
import p.studio.workspaces.framework.StudioEventAware;
import p.studio.workspaces.framework.StudioEventBindings;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;

import static java.util.Comparator.comparingInt;

public final class AssetDetailsControl extends VBox implements StudioEventAware {
    private final ProjectReference projectReference;
    private final StudioWorkspaceEventBus workspaceBus;
    private final StudioEventBindings eventBindings = new StudioEventBindings();
    private final Label detailsTitleLabel = new Label();
    private final Label workspaceSummaryLabel = new Label();
    private final VBox detailsContent = new VBox(12);
    private final ScrollPane detailsScroll = new ScrollPane();
    private final AssetDetailsSummaryControl summaryControl;
    private final AssetDetailsContractControl contractControl;
    private final AssetDetailsBankCompositionControl bankCompositionControl;
    private final AssetDetailsColorSchemaGroupsControl colorSchemaGroupsControl;
    private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl;
    private final AssetDetailsSceneLayersControl sceneLayersControl;
    private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService();
    private final TiledAssetGenerationService tiledGenerationService = new TiledAssetGenerationService();
    private final SceneBankWorkflowService sceneBankWorkflowService = new SceneBankWorkflowService();
    private final VBox actionsContent = new VBox(10);
    private final ScrollPane actionsScroll = new ScrollPane();
    private final VBox actionsSection;
    private final HBox primarySectionsRow = new HBox(12);
    private final AtomicLong loadGeneration = new AtomicLong();

    private AssetWorkspaceDetailsViewState viewState = AssetDetailsViewStateFactory.empty();
    private boolean readyMounted;
    private boolean actionRunning;
    private String actionFeedbackMessage;

    public AssetDetailsControl(
            ProjectReference projectReference,
            StudioWorkspaceEventBus workspaceBus) {
        this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
        this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
        this.summaryControl = new AssetDetailsSummaryControl(projectReference, workspaceBus);
        this.contractControl = new AssetDetailsContractControl(projectReference, workspaceBus);
        this.bankCompositionControl = new AssetDetailsBankCompositionControl(projectReference, workspaceBus);
        this.colorSchemaGroupsControl = new AssetDetailsColorSchemaGroupsControl(projectReference, workspaceBus);
        this.paletteOverhaulingControl = new AssetDetailsPaletteOverhaulingControl(projectReference, workspaceBus);
        this.sceneLayersControl = new AssetDetailsSceneLayersControl(projectReference, workspaceBus);
        this.actionsSection = createActionsSection();

        getStyleClass().add("assets-workspace-pane");
        setSpacing(10);

        detailsTitleLabel.getStyleClass().addAll("assets-workspace-pane-title", "assets-workspace-details-title");

        workspaceSummaryLabel.getStyleClass().add("assets-workspace-summary");
        detailsContent.getStyleClass().add("assets-workspace-details-content");
        primarySectionsRow.getStyleClass().add("assets-details-summary-actions-row");
        primarySectionsRow.setFillHeight(true);
        HBox.setHgrow(summaryControl, Priority.ALWAYS);
        HBox.setHgrow(actionsSection, Priority.ALWAYS);
        summaryControl.setMaxWidth(Double.MAX_VALUE);
        actionsSection.setMaxWidth(Double.MAX_VALUE);
        contractControl.setMaxWidth(Double.MAX_VALUE);
        summaryControl.prefWidthProperty().bind(primarySectionsRow.widthProperty().multiply(0.70d));
        actionsSection.prefWidthProperty().bind(primarySectionsRow.widthProperty().multiply(0.30d));
        primarySectionsRow.getChildren().setAll(summaryControl, actionsSection);
        summaryControl.heightProperty().addListener((ignored, oldValue, newValue) -> syncActionsSectionHeight(newValue.doubleValue()));
        detailsScroll.setContent(detailsContent);
        detailsScroll.setFitToWidth(true);
        detailsScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
        detailsScroll.getStyleClass().add("assets-workspace-details-scroll");

        getChildren().addAll(detailsTitleLabel, workspaceSummaryLabel, detailsScroll);
        VBox.setVgrow(detailsScroll, Priority.ALWAYS);
        render();
    }

    @Override
    public StudioEventBindings eventBindings() {
        return eventBindings;
    }

    @Override
    public void registerEventSubscriptions() {
        eventBindings.listen(workspaceBus, StudioAssetsRefreshRequestedEvent.class).handle(event -> {
            if (event.preferredAssetReference() != null) {
                loadSelection(event.preferredAssetReference());
                return;
            }
            if (event.deepSync() && viewState.selectedAssetReference() != null) {
                loadSelection(viewState.selectedAssetReference());
                return;
            }
            clearSelection();
        });
        eventBindings.listen(workspaceBus, StudioAssetsWorkspaceSelectionRequestedEvent.class).handle(event -> {
            if (event.forceUpdate() || !isCurrentSelection(event.assetReference())) {
                loadSelection(event.assetReference());
            }
        });
    }

    public void scrollToTop() {
        detailsScroll.setVvalue(0.0d);
    }

    private void loadSelection(AssetReference assetReference) {
        final long generation = loadGeneration.incrementAndGet();
        actionRunning = false;
        actionFeedbackMessage = null;
        publishViewState(AssetDetailsViewStateFactory.loading(assetReference));
        Container.backgroundTasks().submit(() -> loadDetails(generation, assetReference));
    }

    private void clearSelection() {
        loadGeneration.incrementAndGet();
        actionRunning = false;
        actionFeedbackMessage = null;
        publishViewState(AssetDetailsViewStateFactory.empty());
    }

    private boolean isCurrentSelection(AssetReference assetReference) {
        if (!Objects.equals(viewState.selectedAssetReference(), assetReference)) {
            return false;
        }
        return viewState.detailsStatus() == AssetWorkspaceDetailsStatus.READY
                || viewState.detailsStatus() == AssetWorkspaceDetailsStatus.LOADING;
    }

    private void loadDetails(long generation, AssetReference assetReference) {
        try {
            final var workspaceService = Container.packer().workspaceService();
            final var response = workspaceService
                    .getAssetDetails(new GetAssetDetailsRequest(
                            projectReference.toPackerProjectContext(),
                            assetReference));
            final var actionsResponse = workspaceService.getAssetActions(new GetAssetActionsRequest(
                    projectReference.toPackerProjectContext(),
                    assetReference));
            final AssetWorkspaceAssetDetails details = mapDetails(
                    response.details(),
                    response.diagnostics(),
                    actionsResponse.actions());
            Platform.runLater(() -> applyLoadedDetails(generation, assetReference, details));
        } catch (RuntimeException exception) {
            Platform.runLater(() -> applyLoadFailure(generation, assetReference, exception));
        }
    }

    private void applyLoadedDetails(
            long generation,
            AssetReference assetReference,
            AssetWorkspaceAssetDetails details) {
        if (generation != loadGeneration.get()) {
            return;
        }
        publishViewState(AssetDetailsViewStateFactory.ready(assetReference, details));
    }

    private void applyLoadFailure(
            long generation,
            AssetReference assetReference,
            RuntimeException exception) {
        if (generation != loadGeneration.get()) {
            return;
        }
        final String message = exception.getMessage() == null || exception.getMessage().isBlank()
                ? Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)
                : exception.getMessage();
        publishViewState(AssetDetailsViewStateFactory.error(assetReference, message));
    }

    private void publishViewState(AssetWorkspaceDetailsViewState nextViewState) {
        final AssetReference previousAssetReference = viewState == null ? null : viewState.selectedAssetReference();
        final String previousScopeKey = previousAssetReference == null ? null : "asset-details:" + previousAssetReference;
        final boolean assetChanged = !Objects.equals(previousAssetReference, nextViewState.selectedAssetReference());
        viewState = Objects.requireNonNull(nextViewState, "nextViewState");
        if (assetChanged || nextViewState.detailsStatus() != AssetWorkspaceDetailsStatus.READY) {
            workspaceBus.publish(new StudioFormEditScopeChangedEvent(
                    previousScopeKey,
                    null));
        }
        render();
        if (assetChanged && nextViewState.selectedAssetReference() != null) {
            Platform.runLater(this::scrollToTop);
        }
        workspaceBus.publish(new StudioAssetsDetailsViewStateChangedEvent(nextViewState));
    }

    private void render() {
        switch (viewState.detailsStatus()) {
            case EMPTY -> {
                readyMounted = false;
                setDetailsTitle(null);
                setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_EMPTY));
                renderActions();
                detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION)));
            }
            case LOADING -> {
                readyMounted = false;
                setDetailsTitle(null);
                setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING));
                renderActions();
                detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)));
            }
            case ERROR -> {
                readyMounted = false;
                setDetailsTitle(null);
                setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_ERROR));
                renderActions();
                detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(
                        Objects.requireNonNullElse(viewState.detailsErrorMessage(), Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY))));
            }
            case READY -> {
                if (viewState.selectedAssetDetails() == null) {
                    readyMounted = false;
                    setDetailsTitle(null);
                    setWorkspaceSummary(null);
                    renderActions();
                    detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION)));
                    return;
                }

                setDetailsTitle(viewState.selectedAssetDetails().summary().assetName());
                setWorkspaceSummary(null);
                renderActions();
                if (!readyMounted) {
                    readyMounted = true;
                    detailsContent.getChildren().setAll(buildDetailsSections());
                }
                syncActionsSectionHeight(summaryControl.getHeight());
            }
        }
    }

    private Node[] buildDetailsSections() {
        final java.util.List<Node> sections = new java.util.ArrayList<>();
        sections.add(primarySectionsRow);
        sections.add(contractControl);
        if (showsSceneLayers(viewState.selectedAssetDetails())) {
            sections.add(sceneLayersControl);
        } else if (!hidesBankComposition(viewState.selectedAssetDetails())) {
            sections.add(bankCompositionControl);
        }
        if (showsColorSchemaGroups(viewState.selectedAssetDetails())) {
            sections.add(colorSchemaGroupsControl);
        }
        sections.add(paletteOverhaulingControl);
        return sections.toArray(Node[]::new);
    }

    private boolean hidesBankComposition(AssetWorkspaceAssetDetails details) {
        return details != null && details.summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK;
    }

    private boolean showsSceneLayers(AssetWorkspaceAssetDetails details) {
        return details != null
                && details.summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK
                && details.sceneBankMetadata() != null;
    }

    private boolean showsColorSchemaGroups(AssetWorkspaceAssetDetails details) {
        return details != null && details.summary().assetFamily() == AssetFamilyCatalog.GLYPH_BANK;
    }

    private VBox createActionsSection() {
        actionsContent.getStyleClass().add("assets-details-actions-content");
        actionsScroll.setContent(actionsContent);
        actionsScroll.setFitToWidth(true);
        actionsScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
        actionsScroll.getStyleClass().add("assets-details-actions-scroll");
        final VBox section = StudioSection.createSectionWithoutHeader(actionsScroll);
        section.getStyleClass().add("assets-details-actions-section");
        return section;
    }

    private void syncActionsSectionHeight(double summaryHeight) {
        if (summaryHeight <= 0.0d) {
            return;
        }
        actionsSection.setMinHeight(summaryHeight);
        actionsSection.setPrefHeight(summaryHeight);
        actionsSection.setMaxHeight(summaryHeight);
    }

    private void setDetailsTitle(String text) {
        detailsTitleLabel.setText(text == null ? "" : text);
        final boolean visible = text != null && !text.isBlank();
        detailsTitleLabel.setVisible(visible);
        detailsTitleLabel.setManaged(visible);
    }

    private void setWorkspaceSummary(String text) {
        workspaceSummaryLabel.setText(text == null ? "" : text);
        final boolean visible = text != null && !text.isBlank();
        workspaceSummaryLabel.setVisible(visible);
        workspaceSummaryLabel.setManaged(visible);
    }

    private void renderActions() {
        actionsContent.getChildren().setAll(buildActionNodes());
    }

    private java.util.List<Node> buildActionNodes() {
        final java.util.List<Node> nodes = new java.util.ArrayList<>();
        if (viewState.selectedAssetDetails() == null) {
            nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY)));
            return nodes;
        }
        final var visibleActions = viewState.selectedAssetDetails().actions().stream()
                .filter(AssetWorkspaceAssetAction::visible)
                .toList();
        if (visibleActions.isEmpty()
                && viewState.selectedAssetDetails().diagnostics().isEmpty()
                && viewState.selectedAssetDetails().summary().state() == p.studio.workspaces.assets.messages.AssetWorkspaceAssetState.UNREGISTERED) {
            nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY)));
            return nodes;
        }
        final Button analyseButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_ANALYSE));
        AssetDetailsUiSupport.applyActionTone(analyseButton, "studio-button-warning");
        analyseButton.setDisable(actionRunning || viewState.selectedAssetDetails().diagnostics().isEmpty());
        analyseButton.setOnAction(ignored -> openDiagnosticsDialog());
        nodes.add(analyseButton);
        if (canGenerateTsx()) {
            final Button exportTsxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_EXPORT_TSX));
            exportTsxButton.setDisable(actionRunning);
            exportTsxButton.setOnAction(ignored -> generateTsx());
            nodes.add(exportTsxButton);
        }
        if (canSceneTmxInteroperability()) {
            final Button importTmxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_IMPORT_TMX));
            importTmxButton.setDisable(actionRunning);
            importTmxButton.setOnAction(ignored -> importTmx());
            nodes.add(importTmxButton);

            final Button exportTmxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_EXPORT_TMX));
            exportTmxButton.setDisable(actionRunning);
            exportTmxButton.setOnAction(ignored -> exportTmx());
            nodes.add(exportTmxButton);
        }
        final Button buildParticipationButton = AssetDetailsUiSupport.createActionButton(buildParticipationActionLabel());
        AssetDetailsUiSupport.applyActionTone(
                buildParticipationButton,
                currentBuildParticipation() == AssetWorkspaceBuildParticipation.INCLUDED
                        ? "studio-button-build-exclude"
                        : "studio-button-build-include");
        buildParticipationButton.setDisable(actionRunning || !canToggleBuildParticipation());
        buildParticipationButton.setOnAction(ignored -> updateBuildParticipation());
        nodes.add(buildParticipationButton);

        final var orderedActions = visibleActions.stream()
                .sorted(comparingInt(action -> switch (action.action()) {
                    case DELETE -> 2;
                    default -> 1;
                }))
                .toList();
        for (AssetWorkspaceAssetAction action : orderedActions) {
            final Button button = AssetDetailsUiSupport.createActionButton(AssetDetailsUiSupport.actionLabel(action.action()));
            if (action.action() == p.packer.messages.assets.AssetAction.DELETE) {
                AssetDetailsUiSupport.applyActionTone(button, "studio-button-danger");
            }
            button.setDisable(actionRunning || !action.enabled());
            button.setOnAction(ignored -> executeAction(action));
            nodes.add(button);
        }
        return nodes;
    }

    private void executeAction(AssetWorkspaceAssetAction action) {
        if (actionRunning || viewState.selectedAssetReference() == null) {
            return;
        }
        switch (action.action()) {
            case REGISTER -> {
                actionRunning = true;
                actionFeedbackMessage = null;
                renderActions();
                Container.backgroundTasks().submit(() -> registerSelectedAsset(viewState.selectedAssetReference()));
            }
            case MOVE -> openMoveWizard();
            case DELETE -> confirmDeleteAction();
        }
    }

    private void openMoveWizard() {
        if (viewState.selectedAssetDetails() == null || getScene() == null || viewState.selectedAssetReference() == null) {
            return;
        }
        MoveAssetWizard.showAndWait(
                getScene().getWindow(),
                projectReference,
                viewState.selectedAssetReference(),
                viewState.selectedAssetDetails().summary()).ifPresent(assetReference -> {
            workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference));
        });
    }

    private void confirmDeleteAction() {
        if (viewState.selectedAssetDetails() == null || getScene() == null) {
            return;
        }
        final boolean confirmed = DeleteAssetDialog.showAndWait(
                getScene().getWindow(),
                viewState.selectedAssetDetails().summary().assetName());
        if (!confirmed) {
            return;
        }
        actionRunning = true;
        actionFeedbackMessage = null;
        renderActions();
        final AssetReference assetReference = viewState.selectedAssetReference();
        Container.backgroundTasks().submit(() -> deleteSelectedAsset(assetReference));
    }

    private void openDiagnosticsDialog() {
        if (viewState.selectedAssetDetails() == null || getScene() == null) {
            return;
        }
        AssetDiagnosticsDialog.showAndWait(
                getScene().getWindow(),
                projectReference,
                viewState.selectedAssetDetails().summary().assetName(),
                viewState.selectedAssetDetails().diagnostics());
    }

    private boolean canToggleBuildParticipation() {
        return viewState.selectedAssetDetails() != null
                && viewState.selectedAssetDetails().summary().state() == p.studio.workspaces.assets.messages.AssetWorkspaceAssetState.REGISTERED
                && (!isSceneBankAwaitingActiveLayerForBuild());
    }

    private boolean isSceneBankAwaitingActiveLayerForBuild() {
        return viewState.selectedAssetDetails() != null
                && viewState.selectedAssetDetails().summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK
                && currentBuildParticipation() != AssetWorkspaceBuildParticipation.INCLUDED
                && (viewState.selectedAssetDetails().sceneBankMetadata() == null
                || viewState.selectedAssetDetails().sceneBankMetadata().enabledLayerCount() == 0);
    }

    private AssetWorkspaceBuildParticipation currentBuildParticipation() {
        return viewState.selectedAssetDetails().summary().buildParticipation();
    }

    private String buildParticipationActionLabel() {
        return currentBuildParticipation() == AssetWorkspaceBuildParticipation.INCLUDED
                ? Container.i18n().text(I18n.ASSETS_ACTION_EXCLUDE_FROM_BUILD)
                : Container.i18n().text(I18n.ASSETS_ACTION_INCLUDE_IN_BUILD);
    }

    private void updateBuildParticipation() {
        if (actionRunning || viewState.selectedAssetReference() == null || !canToggleBuildParticipation()) {
            return;
        }
        actionRunning = true;
        actionFeedbackMessage = null;
        renderActions();
        final AssetReference assetReference = viewState.selectedAssetReference();
        final boolean includeInBuild = currentBuildParticipation() != AssetWorkspaceBuildParticipation.INCLUDED;
        Container.backgroundTasks().submit(() -> {
            try {
                final UpdateAssetBuildParticipationResult result = Container.packer().workspaceService()
                        .updateAssetBuildParticipation(new UpdateAssetBuildParticipationRequest(
                                projectReference.toPackerProjectContext(),
                                assetReference,
                                includeInBuild));
                Platform.runLater(() -> applyBuildParticipationResult(result));
            } catch (RuntimeException exception) {
                Platform.runLater(() -> applyBuildParticipationFailure(exception));
            }
        });
    }

    private boolean canGenerateTsx() {
        return viewState.selectedAssetDetails() != null
                && viewState.selectedAssetDetails().summary().assetFamily() == p.packer.messages.assets.AssetFamilyCatalog.GLYPH_BANK
                && viewState.selectedAssetDetails().summary().glyphSpecialization() == AssetStudioGlyphSpecialization.TILESET;
    }

    private boolean canSceneTmxInteroperability() {
        return viewState.selectedAssetDetails() != null
                && viewState.selectedAssetDetails().summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK
                && viewState.selectedAssetDetails().sceneBankMetadata() != null;
    }

    private void generateTsx() {
        if (actionRunning || viewState.selectedAssetDetails() == null) {
            return;
        }
        actionRunning = true;
        actionFeedbackMessage = null;
        renderActions();
        final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
        Container.backgroundTasks().submit(() -> {
            final TiledAssetGenerationResult result = tiledGenerationService.exportTilesetTsx(details);
            Platform.runLater(() -> applyTiledGenerationResult("glyph-bank-tileset-export", result));
        });
    }

    private void exportTmx() {
        if (actionRunning || viewState.selectedAssetDetails() == null) {
            return;
        }
        final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
        final SceneTmxExportDisposition disposition = chooseSceneTmxExportDisposition(details);
        if (disposition == null) {
            return;
        }
        actionRunning = true;
        actionFeedbackMessage = null;
        Container.backgroundTasks().submit(() -> {
            final TiledAssetGenerationResult result = tiledGenerationService.exportSceneBankTmx(projectReference, details, disposition);
            Platform.runLater(() -> applyTiledGenerationResult("scene-bank-tmx-export", result));
        });
    }

    private SceneTmxExportDisposition chooseSceneTmxExportDisposition(AssetWorkspaceAssetDetails details) {
        final Path sceneTmxPath = resolveSceneTmxPath(details);
        if (sceneTmxPath == null || !Files.isRegularFile(sceneTmxPath)) {
            return SceneTmxExportDisposition.OVERWRITE;
        }
        final Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
        if (getScene() != null) {
            alert.initOwner(getScene().getWindow());
        }
        final ButtonType overwriteButton = new ButtonType("Overwrite", ButtonBar.ButtonData.YES);
        final ButtonType keepBothButton = new ButtonType("Keep Both", ButtonBar.ButtonData.NO);
        final ButtonType cancelButton = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
        alert.setTitle("Export TMX");
        alert.setHeaderText(null);
        alert.setContentText("A scene.tmx file already exists. Choose how to proceed.");
        alert.getButtonTypes().setAll(overwriteButton, keepBothButton, cancelButton);
        final ButtonType result = alert.showAndWait().orElse(cancelButton);
        if (result == overwriteButton) {
            return SceneTmxExportDisposition.OVERWRITE;
        }
        if (result == keepBothButton) {
            return SceneTmxExportDisposition.KEEP_BOTH;
        }
        return null;
    }

    private Path resolveSceneTmxPath(AssetWorkspaceAssetDetails details) {
        if (details == null || details.sceneBankMetadata() == null) {
            return null;
        }
        final LinkedHashSet<String> tilemaps = new LinkedHashSet<>();
        details.sceneBankMetadata().layerBindings().forEach(binding -> {
            final String tilemap = Objects.requireNonNullElse(binding.tilemap(), "").trim();
            if (!tilemap.isBlank()) {
                tilemaps.add(tilemap);
            }
        });
        if (tilemaps.size() != 1) {
            return null;
        }
        return details.summary().assetRoot().resolve(tilemaps.getFirst());
    }

    private void importTmx() {
        if (actionRunning || viewState.selectedAssetDetails() == null) {
            return;
        }
        actionRunning = true;
        actionFeedbackMessage = null;
        final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
        Container.backgroundTasks().submit(() -> {
            final TiledAssetGenerationResult result = tiledGenerationService.importSceneBankTmx(projectReference, details);
            Platform.runLater(() -> applyTiledGenerationResult("scene-bank-tmx-import", result));
        });
    }

    private void applyTiledGenerationResult(String source, TiledAssetGenerationResult result) {
        actionRunning = false;
        actionFeedbackMessage = result.message();
        workspaceBus.publish(new StudioAssetLogEvent(source, result.message()));
        if (result.success()) {
            if (CollectionUtils.isNotEmpty(result.updatedBindings())) {
                final var sceneBankMetadata = viewState.selectedAssetDetails().sceneBankMetadata().withUpdatedBindings(result.updatedBindings());
                final var updatedAssetDetails = viewState.selectedAssetDetails().withSceneBankMetadata(sceneBankMetadata);
                viewState = viewState.withSelectedAssetReference(updatedAssetDetails);
            }
            workspaceBus.publish(new StudioAssetsWorkspaceDetailsSceneLayerChangedEvent(viewState));
        }
    }

    private void applyBuildParticipationResult(UpdateAssetBuildParticipationResult result) {
        actionRunning = false;
        if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) {
            actionFeedbackMessage = null;
            workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(result.assetReference()));
            return;
        }
        actionFeedbackMessage = result.summary();
        renderActions();
    }

    private void applyBuildParticipationFailure(RuntimeException exception) {
        actionRunning = false;
        actionFeedbackMessage = exception.getMessage() == null || exception.getMessage().isBlank()
                ? "Unable to update asset build participation."
                : exception.getMessage();
        renderActions();
    }

    private void registerSelectedAsset(AssetReference assetReference) {
        try {
            final RegisterAssetResult result = Container.packer().workspaceService().registerAsset(new RegisterAssetRequest(
                    projectReference.toPackerProjectContext(),
                    assetReference));
            Platform.runLater(() -> applyRegisterResult(result));
        } catch (RuntimeException exception) {
            Platform.runLater(() -> applyRegisterFailure(exception));
        }
    }

    private void applyRegisterResult(RegisterAssetResult result) {
        actionRunning = false;
        if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) {
            actionFeedbackMessage = null;
            workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(result.assetReference()));
            return;
        }
        actionFeedbackMessage = Objects.requireNonNullElse(result.summary(), "Unable to register asset.");
        renderActions();
    }

    private void applyRegisterFailure(RuntimeException exception) {
        actionRunning = false;
        actionFeedbackMessage = exception.getMessage() == null || exception.getMessage().isBlank()
                ? "Unable to register asset."
                : exception.getMessage();
        renderActions();
    }

    private void deleteSelectedAsset(AssetReference assetReference) {
        try {
            final DeleteAssetResult result = Container.packer().workspaceService().deleteAsset(new DeleteAssetRequest(
                    projectReference.toPackerProjectContext(),
                    assetReference));
            Platform.runLater(() -> applyDeleteResult(result));
        } catch (RuntimeException exception) {
            Platform.runLater(() -> applyDeleteFailure(exception));
        }
    }

    private void applyDeleteResult(DeleteAssetResult result) {
        actionRunning = false;
        if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS) {
            actionFeedbackMessage = null;
            workspaceBus.publish(new StudioAssetsRefreshRequestedEvent());
            return;
        }
        actionFeedbackMessage = Objects.requireNonNullElse(result.summary(), "Unable to delete asset.");
        renderActions();
    }

    private void applyDeleteFailure(RuntimeException exception) {
        actionRunning = false;
        actionFeedbackMessage = exception.getMessage() == null || exception.getMessage().isBlank()
                ? "Unable to delete asset."
                : exception.getMessage();
        renderActions();
    }

    private AssetWorkspaceAssetDetails mapDetails(
            PackerAssetDetailsDTO details,
            java.util.List<PackerDiagnosticDTO> diagnostics,
            java.util.List<PackerAssetActionAvailabilityDTO> actions) {
        final var baseSummary = AssetListPackerMappings.mapSummary(details.summary());
        final AssetStudioMetadataSnapshot studioMetadata = studioMetadataService.read(
                baseSummary.assetRoot(),
                baseSummary.assetFamily());
        final java.util.List<AssetWorkspaceAssetAction> mappedActions = actions.stream()
                .map(action -> new AssetWorkspaceAssetAction(
                        action.action(),
                        action.enabled(),
                        action.visible(),
                        action.reason()))
                .toList();
        final p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary mappedSummary =
                new p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary(
                        baseSummary.assetReference(),
                        baseSummary.assetName(),
                        baseSummary.state(),
                        baseSummary.buildParticipation(),
                        baseSummary.assetId(),
                        baseSummary.assetFamily(),
                        studioMetadata.glyphSpecialization(),
                        baseSummary.assetRoot(),
                        baseSummary.preload(),
                        baseSummary.hasDiagnostics());
        final AssetWorkspaceBankCompositionDetails bankComposition = mapBankComposition(details.bankComposition());
        final AssetWorkspaceAssetDetails draftDetails = new AssetWorkspaceAssetDetails(
                mappedSummary,
                mappedActions,
                details.outputFormat(),
                details.outputCodec(),
                details.availableOutputCodecs(),
                details.codecConfigurationFieldsByCodec(),
                details.metadataFields(),
                details.outputPipeline(),
                bankComposition,
                details.pipelinePalettes(),
                studioMetadata.glyphMaterialMetadata(),
                studioMetadata.sceneBankMetadata(),
                AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
                List.of());
        final SceneBankWorkflowResult sceneBankWorkflow = sceneBankWorkflowService.inspect(projectReference, draftDetails);
        final java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics());
        for (PackerDiagnosticDTO diagnostic : diagnostics) {
            if (!mergedDiagnostics.contains(diagnostic)) {
                mergedDiagnostics.add(diagnostic);
            }
        }
        for (PackerDiagnosticDTO diagnostic : sceneBankWorkflow.diagnostics()) {
            if (!mergedDiagnostics.contains(diagnostic)) {
                mergedDiagnostics.add(diagnostic);
            }
        }
        return new AssetWorkspaceAssetDetails(
                mappedSummary,
                mappedActions,
                details.outputFormat(),
                details.outputCodec(),
                details.availableOutputCodecs(),
                details.codecConfigurationFieldsByCodec(),
                details.metadataFields(),
                details.outputPipeline(),
                bankComposition,
                details.pipelinePalettes(),
                studioMetadata.glyphMaterialMetadata(),
                studioMetadata.sceneBankMetadata(),
                sceneBankWorkflow.validation(),
                mergedDiagnostics);
    }

    private AssetWorkspaceBankCompositionDetails mapBankComposition(p.packer.dtos.PackerBankCompositionDetailsDTO details) {
        return new AssetWorkspaceBankCompositionDetails(
                details.availableFiles().stream().map(this::mapBankCompositionFile).toList(),
                details.selectedFiles().stream().map(this::mapBankCompositionFile).toList(),
                details.measuredBankSizeBytes());
    }

    private AssetWorkspaceBankCompositionFile mapBankCompositionFile(p.packer.dtos.PackerBankCompositionFileDTO file) {
        return new AssetWorkspaceBankCompositionFile(
                file.path(),
                file.displayName(),
                file.size(),
                file.lastModified(),
                file.fingerprint(),
                file.metadata());
    }
}
