package p.studio.workspaces.assets.details.schema;

import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import p.studio.Container;
import p.studio.controls.banks.StudioDualListView;
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
import p.studio.controls.forms.StudioFormMode;
import p.studio.controls.forms.StudioFormSection;
import p.studio.controls.forms.StudioFormSession;
import p.studio.lsp.events.StudioWorkspaceEventBus;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
import p.studio.workspaces.assets.messages.events.StudioGlyphMaterialMetadataChangedEvent;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialArtifactBinding;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchema;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchemaStatus;
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
import p.studio.workspaces.framework.StudioSubscriptionBag;

import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

public final class AssetDetailsColorSchemaGroupsControl extends StudioFormSection {
    private static final String SECTION_ID = "asset-details.color-schema-groups";

    private final StudioWorkspaceEventBus workspaceBus;
    private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
    private final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
    private final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
    private final ListView<SchemaListItem> schemaListView = new ListView<>();
    private final SchemaTileDualListView tileDualListView = new SchemaTileDualListView();
    private final VBox schemaActions = new VBox(6);
    private final HBox schemaListPane = new HBox(8, schemaActions, schemaListView);
    private final HBox paletteStrip = new HBox(4);
    private final Label summaryLabel = new Label();
    private final Label schemaTitleLabel = new Label();
    private final Label paletteCounterLabel = new Label();
    private final Label warningLabel = new Label();
    private final VBox editorPane = new VBox(10);
    private final HBox body = new HBox(16);
    private final VBox content = new VBox(12, body, summaryLabel);

    private AssetWorkspaceDetailsViewState viewState;
    private StudioFormSession<AssetDetailsColorSchemaDraft> formSession;
    private String selectedSchemaId;
    private boolean selectingSchema;

    public AssetDetailsColorSchemaGroupsControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) {
        Objects.requireNonNull(projectReference, "projectReference");
        this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
        schemaListView.getStyleClass().add("assets-details-color-schema-list");
        schemaListView.setCellFactory(ignored -> createSchemaCell());
        schemaListView.getSelectionModel().selectedItemProperty().addListener((ignored, previous, current) -> {
            if (selectingSchema) {
                return;
            }
            selectedSchemaId = current == null ? null : current.schemaId();
            if (formMode() == StudioFormMode.EDITING) {
                formSession.setDraft(selectedSchemaId == null
                        ? AssetDetailsColorSchemaDraft.noSelection()
                        : sourceDraft(selectedSchemaId).asUpdateDraft());
            } else {
                replaceFormSource();
            }
            renderSection();
        });
        schemaActions.getStyleClass().add("assets-details-color-schema-toolbar");
        schemaListPane.getStyleClass().add("assets-details-list-with-actions");
        content.getStyleClass().add("assets-details-color-schema-content");
        body.getStyleClass().add("assets-details-color-schema-body");
        paletteStrip.getStyleClass().add("assets-details-color-schema-strip");
        schemaTitleLabel.getStyleClass().add("assets-details-color-schema-title");
        paletteCounterLabel.getStyleClass().add("assets-details-color-schema-summary");
        summaryLabel.getStyleClass().add("assets-details-color-schema-summary");
        warningLabel.getStyleClass().add("assets-details-color-schema-warning");
        HBox.setHgrow(schemaListPane, Priority.ALWAYS);
        HBox.setHgrow(schemaListView, Priority.ALWAYS);
        HBox.setHgrow(editorPane, Priority.ALWAYS);
        schemaListView.setMaxWidth(Double.MAX_VALUE);
        editorPane.setMaxWidth(Double.MAX_VALUE);
    }

    @Override
    public void subscribe() {
        subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> {
            viewState = event.viewState();
            coordinator.replaceDetails(viewState == null ? null : viewState.selectedAssetDetails());
            normalizeSelectedSchema();
            replaceFormSource();
            if (viewState == null || viewState.selectedAssetReference() == null) {
                publishEditScope(workspaceBus, currentScopeKey(), null);
            }
            renderSection();
        }));
        subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
                event -> handleEditScopeChanged(event, currentScopeKey())));
        subscriptions.add(workspaceBus.subscribe(StudioGlyphMaterialMetadataChangedEvent.class, event -> {
            if (viewState == null || !Objects.equals(viewState.selectedAssetReference(), event.assetReference())) {
                return;
            }
            coordinator.replaceMaterialMetadata(event.metadata());
            if (formMode() != StudioFormMode.EDITING) {
                replaceFormSource();
            }
            renderSection();
        }));
    }

    @Override
    public void unsubscribe() {
        subscriptions.clear();
    }

    @Override
    protected void renderSection() {
        if (viewState == null || viewState.selectedAssetDetails() == null || !coordinator.ready()) {
            formSession = null;
            clearRenderedSection();
            return;
        }
        if (formSession == null) {
            replaceFormSource();
        }

        final AssetDetailsColorSchemaDraft draft = formSession.draft();
        renderSchemaList();
        renderSchemaActions(draft);
        renderEditor(draft);
        summaryLabel.setText(Container.i18n().format(
                I18n.ASSETS_COLOR_SCHEMA_SUMMARY,
                coordinator.schemas().size(),
                coordinator.unassignedTileCount()));
        body.getChildren().setAll(schemaListPane, editorPane);
        renderFormSection(content);
    }

    @Override
    protected String sectionTitle() {
        return Container.i18n().text(I18n.ASSETS_SECTION_COLOR_SCHEMA_GROUPS);
    }

    @Override
    protected StudioFormMode formMode() {
        return formSession == null ? StudioFormMode.READ_ONLY : formSession.mode();
    }

    @Override
    protected boolean isDirty() {
        return formSession != null && formSession.isDirty();
    }

    @Override
    protected void beginEdit() {
        if (formSession == null) {
            replaceFormSource();
        }
        selectedSchemaId = null;
        formSession.replaceSource(AssetDetailsColorSchemaDraft.noSelection());
        formSession.beginEdit();
        publishEditScope(workspaceBus, currentScopeKey(), SECTION_ID);
    }

    @Override
    protected void apply() {
        if (viewState == null || viewState.selectedAssetDetails() == null || viewState.selectedAssetReference() == null || formSession == null) {
            return;
        }
        final AssetDetailsColorSchemaDraft draft = formSession.draft();
        if (!draft.selected()) {
            workspaceBus.publish(new StudioAssetLogEvent("color-schema-groups", "Apply failed: select or create a schema first."));
            return;
        }
        if (!draft.removeRequested()) {
            final List<String> orderedColors = draft.orderedColors();
            if (draft.assignedTiles().isEmpty() || orderedColors.isEmpty()) {
                workspaceBus.publish(new StudioAssetLogEvent("color-schema-groups", "Apply failed: schema must contain at least one tile."));
                return;
            }
            if (orderedColors.size() > 15) {
                workspaceBus.publish(new StudioAssetLogEvent(
                        "color-schema-groups",
                        Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_TOO_MANY_COLORS)));
                return;
            }
        }

        final var assetDetails = viewState.selectedAssetDetails();
        final var assetReference = viewState.selectedAssetReference();
        Container.backgroundTasks().submit(() -> {
            try {
                final AssetStudioGlyphMaterialMetadata metadata = draft.removeRequested()
                        ? coordinator.removeSchema(draft.schemaId())
                        : coordinator.upsertSchema(draft.schemaId(), draft.assignedTiles(), draft.orderedColors());
                metadataService.writeGlyphMaterialMetadata(assetDetails.summary().assetRoot(), metadata);
                Platform.runLater(() -> {
                    formSession.apply();
                    selectedSchemaId = draft.removeRequested() ? null : draft.schemaId();
                    publishEditScope(workspaceBus, currentScopeKey(), null);
                    workspaceBus.publish(new StudioAssetLogEvent(
                            "color-schema-groups",
                            draft.removeRequested() ? "Schema removed: " + draft.schemaId() : "Schema saved: " + draft.schemaId()));
                    workspaceBus.publish(new StudioGlyphMaterialMetadataChangedEvent(assetReference, metadata));
                    renderSection();
                });
            } catch (IOException | RuntimeException exception) {
                Platform.runLater(() -> workspaceBus.publish(new StudioAssetLogEvent(
                        "color-schema-groups",
                        "Unable to save schema: " + Objects.requireNonNullElse(exception.getMessage(), "unknown error"))));
            }
        });
    }

    @Override
    protected void reset() {
        if (formSession == null) {
            return;
        }
        formSession.resetDraft();
        selectedSchemaId = formSession.draft().selected() ? formSession.draft().schemaId() : null;
    }

    @Override
    protected void cancel() {
        if (formSession == null) {
            return;
        }
        formSession.cancelEdit();
        selectedSchemaId = formSession.draft().selected() ? formSession.draft().schemaId() : null;
        publishEditScope(workspaceBus, currentScopeKey(), null);
    }

    @Override
    protected String sectionStyleClass() {
        return "assets-details-color-schema-section";
    }

    @Override
    protected String formSectionId() {
        return SECTION_ID;
    }

    private void renderSchemaList() {
        selectingSchema = true;
        schemaListView.getItems().setAll(schemaListItems());
        schemaListView.setDisable(false);
        if (selectedSchemaId != null) {
            schemaListView.getItems().stream()
                    .filter(schema -> schema.schemaId().equals(selectedSchemaId))
                    .findFirst()
                    .ifPresentOrElse(
                            schema -> schemaListView.getSelectionModel().select(schema),
                            () -> schemaListView.getSelectionModel().clearSelection());
        } else {
            schemaListView.getSelectionModel().clearSelection();
        }
        selectingSchema = false;
    }

    private void renderSchemaActions(AssetDetailsColorSchemaDraft draft) {
        final Button newButton = new Button("+");
        newButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "assets-details-mini-action-button");
        newButton.setDisable(formMode() != StudioFormMode.EDITING);
        newButton.setOnAction(ignored -> {
            selectedSchemaId = coordinator.nextSchemaId();
            formSession.setDraft(sourceDraft(selectedSchemaId).asUpdateDraft());
            rerenderPreservingScrollPosition();
        });

        final Button removeButton = new Button("-");
        removeButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "assets-details-mini-action-button");
        removeButton.setDisable(formMode() != StudioFormMode.EDITING || !draft.selected() || (draft.existingSchema() && !coordinator.canRemove(draft.schemaId())));
        removeButton.setOnAction(ignored -> {
            if (draft.existingSchema()) {
                formSession.updateDraft(AssetDetailsColorSchemaDraft::markForRemoval);
            } else {
                selectedSchemaId = null;
                formSession.setDraft(AssetDetailsColorSchemaDraft.noSelection());
            }
            rerenderPreservingScrollPosition();
        });
        if (draft.existingSchema() && !coordinator.canRemove(draft.schemaId())) {
            removeButton.setTooltip(new javafx.scene.control.Tooltip(
                    Container.i18n().format(I18n.ASSETS_COLOR_SCHEMA_REMOVE_BLOCKED, draft.schemaId())));
        }

        schemaActions.getChildren().setAll(newButton, removeButton);
    }

    private void renderEditor(AssetDetailsColorSchemaDraft draft) {
        editorPane.getChildren().clear();
        if (!draft.selected()) {
            tileDualListView.setLeftItems(List.of());
            tileDualListView.setRightItems(List.of());
            tileDualListView.setInteractionEnabled(false);
            paletteStrip.getChildren().clear();
            paletteCounterLabel.setText(Container.i18n().format(I18n.ASSETS_COLOR_SCHEMA_DIALOG_COLOR_COUNT, 0));
            editorPane.getChildren().addAll(
                    emptyMessage("Select a schema or create a new one to edit its tile group."),
                    tileDualListView,
                    paletteCounterLabel,
                    paletteStrip);
            return;
        }
        if (draft.removeRequested()) {
            editorPane.getChildren().add(emptyMessage("Schema " + draft.schemaId() + " will be removed when you apply this form."));
            return;
        }

        final List<String> orderedColors = draft.orderedColors();
        schemaTitleLabel.setText(draft.schemaId() + (draft.existingSchema() ? "" : " (new)"));
        paletteCounterLabel.setText(Container.i18n().format(I18n.ASSETS_COLOR_SCHEMA_DIALOG_COLOR_COUNT, orderedColors.size()));
        warningLabel.setText(orderedColors.size() > 15
                ? Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_TOO_MANY_COLORS)
                : "");
        paletteStrip.getChildren().setAll(orderedColors.stream().map(this::swatch).toList());

        tileDualListView.setLeftItems(draft.availableTiles());
        tileDualListView.setRightItems(draft.assignedTiles());
        tileDualListView.setInteractionEnabled(formMode() == StudioFormMode.EDITING && draft.selected());
        tileDualListView.setMoveToRightAllowed(true);
        tileDualListView.setOnMoveToRight(items -> updateDraft(current -> current.moveToAssigned(items)));
        tileDualListView.setOnMoveToLeft(items -> updateDraft(current -> current.moveToAvailable(items)));
        tileDualListView.setOnMoveUp(index -> updateDraft(current -> current.moveAssignedUp(index)));
        tileDualListView.setOnMoveDown(index -> updateDraft(current -> current.moveAssignedDown(index)));

        editorPane.getChildren().addAll(schemaTitleLabel, tileDualListView, paletteCounterLabel, paletteStrip, warningLabel);
        VBox.setVgrow(tileDualListView, Priority.ALWAYS);
    }

    private void updateDraft(java.util.function.UnaryOperator<AssetDetailsColorSchemaDraft> update) {
        if (formMode() != StudioFormMode.EDITING || formSession == null) {
            return;
        }
        formSession.updateDraft(update);
        rerenderPreservingScrollPosition();
    }

    private AssetDetailsColorSchemaDraft sourceDraft(String schemaId) {
        final AssetStudioGlyphMaterialSchema schema = coordinator.findSchema(schemaId).orElse(null);
        final List<AssetWorkspaceBankCompositionFile> selectableTiles = schema == null
                ? coordinator.availableTilesForCreate()
                : coordinator.availableTilesForEdit(schema.schemaId());
        final Set<String> assignedArtifactIds = schema == null
                ? Set.of()
                : schema.artifactBindings().stream()
                        .map(AssetStudioGlyphMaterialArtifactBinding::artifactId)
                        .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
        final List<AssetWorkspaceBankCompositionFile> assignedTiles = selectableTiles.stream()
                .filter(file -> assignedArtifactIds.contains(AssetDetailsColorSchemaCoordinator.artifactId(file)))
                .toList();
        final List<AssetWorkspaceBankCompositionFile> availableTiles = selectableTiles.stream()
                .filter(file -> !assignedArtifactIds.contains(AssetDetailsColorSchemaCoordinator.artifactId(file)))
                .toList();
        final List<String> preferredColors = schema == null
                ? List.of()
                : schema.materialIndices().entrySet().stream()
                        .sorted(java.util.Map.Entry.comparingByKey())
                        .map(java.util.Map.Entry::getValue)
                        .toList();
        return new AssetDetailsColorSchemaDraft(schemaId, schema != null, false, availableTiles, assignedTiles, preferredColors, schema == null);
    }

    private void replaceFormSource() {
        if (!coordinator.ready()) {
            formSession = null;
            return;
        }
        if (formSession != null && formSession.mode() == StudioFormMode.EDITING) {
            return;
        }
        final String schemaId = selectedSchemaId != null ? selectedSchemaId : defaultSchemaId();
        selectedSchemaId = schemaId;
        final AssetDetailsColorSchemaDraft source = schemaId == null
                ? AssetDetailsColorSchemaDraft.noSelection()
                : sourceDraft(schemaId);
        if (formSession == null) {
            formSession = new StudioFormSession<>(source);
        } else {
            formSession.replaceSource(source);
        }
    }

    private void normalizeSelectedSchema() {
        if (selectedSchemaId == null || coordinator.findSchema(selectedSchemaId).isPresent()) {
            return;
        }
        selectedSchemaId = null;
    }

    private String defaultSchemaId() {
        return coordinator.schemas().stream()
                .map(AssetStudioGlyphMaterialSchema::schemaId)
                .findFirst()
                .orElse(null);
    }

    private List<SchemaListItem> schemaListItems() {
        final List<SchemaListItem> items = new java.util.ArrayList<>(coordinator.schemas().stream()
                .map(schema -> SchemaListItem.fromSchema(schema, coordinator.selectedTiles()))
                .toList());
        if (formSession != null
                && formSession.draft().selected()
                && !formSession.draft().existingSchema()
                && items.stream().noneMatch(item -> item.schemaId().equals(formSession.draft().schemaId()))) {
            items.add(SchemaListItem.fromDraft(formSession.draft()));
        }
        return List.copyOf(items);
    }

    private ListCell<SchemaListItem> createSchemaCell() {
        return new ListCell<>() {
            @Override
            protected void updateItem(SchemaListItem item, boolean empty) {
                super.updateItem(item, empty);
                if (empty || item == null) {
                    setGraphic(null);
                    setText(null);
                    return;
                }
                final Label title = new Label(item.schemaId());
                title.getStyleClass().add("assets-details-color-schema-title");
                final Label version = new Label(item.existing() ? "v" + item.version() : "new");
                version.getStyleClass().add("assets-details-color-schema-version");
                final Label status = new Label(item.status());
                status.getStyleClass().addAll("studio-chip", Objects.equals(item.status(), AssetStudioGlyphMaterialSchemaStatus.OK.manifestValue())
                        ? "assets-details-color-schema-status-ok"
                        : "assets-details-color-schema-status-inconsistent");
                final Region spacer = new Region();
                HBox.setHgrow(spacer, Priority.ALWAYS);
                final HBox top = new HBox(8, title, version, spacer, status);

                final Label meta = new Label(item.tileCount() + " tiles · " + item.indexCount() + " indices");
                meta.getStyleClass().add("assets-details-color-schema-meta");
                final VBox root = new VBox(6, top, meta);
                root.getStyleClass().add("assets-details-color-schema-item");
                setGraphic(root);
                setText(null);
            }
        };
    }

    private Region swatch(String color) {
        final Region swatch = new Region();
        swatch.getStyleClass().add("assets-details-color-schema-swatch");
        swatch.setMinSize(20, 20);
        swatch.setPrefSize(20, 20);
        swatch.setMaxSize(20, 20);
        swatch.setStyle("-fx-background-color: " + color + ";");
        return swatch;
    }

    private Node emptyMessage(String text) {
        final Label label = new Label(text);
        label.getStyleClass().add("assets-details-color-schema-empty");
        return label;
    }

    private String currentScopeKey() {
        return viewState == null || viewState.selectedAssetReference() == null
                ? null
                : "asset-details:" + viewState.selectedAssetReference();
    }

    private static final class SchemaTileDualListView extends StudioDualListView<AssetWorkspaceBankCompositionFile> {
        private SchemaTileDualListView() {
            setLeftTitle(Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_AVAILABLE_TILES));
            setRightTitle(Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_ASSIGNED_TILES));
        }

        @Override
        protected String itemText(AssetWorkspaceBankCompositionFile item) {
            return item.displayName();
        }
    }

    private record SchemaListItem(
            String schemaId,
            boolean existing,
            int version,
            String status,
            int tileCount,
            int indexCount) {

        private static SchemaListItem fromSchema(AssetStudioGlyphMaterialSchema schema, List<AssetWorkspaceBankCompositionFile> selectedTiles) {
            return new SchemaListItem(
                    schema.schemaId(),
                    true,
                    schema.version(),
                    AssetDetailsColorSchemaCoordinator.effectiveStatus(schema, selectedTiles).manifestValue(),
                    schema.artifactBindings().size(),
                    schema.materialIndices().size());
        }

        private static SchemaListItem fromDraft(AssetDetailsColorSchemaDraft draft) {
            return new SchemaListItem(
                    draft.schemaId(),
                    false,
                    0,
                    AssetStudioGlyphMaterialSchemaStatus.OK.manifestValue(),
                    draft.assignedTiles().size(),
                    draft.orderedColors().size());
        }
    }
}
