package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;

import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;

/**
 * A container that switches between a non-editable {@link ComboBox} and a manual {@link TextField}
 * for model identifier input, depending on the current {@link GuiModelSource}.
 * <p>
 * When the model source is {@link GuiModelSource#LIST_REMOTE_SUCCESS}, a non-editable
 * {@code ComboBox} is shown, pre-populated with the remote list and with the first model
 * pre-selected. In all other cases (including {@link GuiModelSource#NOT_YET_LOADED}) the
 * manual text field is shown, which may be empty or disabled depending on the source state.
 * <p>
 * Exactly one child is {@code visible} and {@code managed} at any time. The other child is
 * kept in the scene graph with both flags set to {@code false} so that no blank space appears.
 * <p>
 * This class contains JavaFX references and must only be used on the JavaFX Application Thread.
 */
public final class GuiModelFieldContainer extends StackPane {

    private final ComboBox<String> comboBox;
    private final TextField textField;
    private final Consumer<String> onModelChange;

    private GuiModelSource currentSource;

    /**
     * Guard flag that suppresses the change callback while the text field value is being set
     * programmatically via {@link #setTextFieldValue(String)}. The callback must still fire on
     * genuine user edits, so the guard is scoped tightly around the programmatic write only.
     */
    private boolean programmaticTextFieldSet = false;

    /**
     * Creates a new model field container.
     *
     * @param initialModelValue the initial model text shown in the text field; may be blank
     * @param onModelChange     callback invoked on every model-value change; must not be {@code null}
     */
    public GuiModelFieldContainer(String initialModelValue, Consumer<String> onModelChange) {
        this.onModelChange = Objects.requireNonNull(onModelChange, "onModelChange must not be null");
        this.currentSource = GuiModelSource.NOT_YET_LOADED;

        this.textField = new TextField(initialModelValue == null ? "" : initialModelValue);
        this.textField.textProperty().addListener((obs, oldText, newText) -> {
            if (!programmaticTextFieldSet && !newText.equals(oldText)) {
                onModelChange.accept(newText);
            }
        });

        this.comboBox = new ComboBox<>();
        this.comboBox.setEditable(false);
        this.comboBox.valueProperty().addListener((obs, oldVal, newVal) -> {
            if (newVal != null && !newVal.equals(oldVal)) {
                onModelChange.accept(newVal);
            }
        });

        // Initial state: show text field (NOT_YET_LOADED → manual input)
        applyVisibility(false);
        setAlignment(Pos.CENTER_LEFT);
        getChildren().addAll(comboBox, textField);
    }

    /**
     * Returns the currently displayed model value.
     * <p>
     * When the {@code ComboBox} is active, returns the selected item. When the text field
     * is active, returns the text field content. Never returns {@code null}.
     *
     * @return the current model value; never {@code null}
     */
    public String currentModelValue() {
        if (currentSource == GuiModelSource.LIST_REMOTE_SUCCESS) {
            String val = comboBox.getValue();
            return val == null ? "" : val;
        }
        return textField.getText() == null ? "" : textField.getText();
    }

    /**
     * Returns the current model source state.
     *
     * @return the current {@link GuiModelSource}; never {@code null}
     */
    public GuiModelSource currentSource() {
        return currentSource;
    }

    /**
     * Applies a successful model list and switches to the non-editable {@link ComboBox}.
     * <p>
     * If the previously active manual text value is present in the new list it is kept as the
     * selection; otherwise the first model in the list is pre-selected and the former manual
     * value is discarded.
     * <p>
     * Must be called on the JavaFX Application Thread.
     *
     * @param models           non-empty list of model identifiers; must not be {@code null} or empty
     * @param previousManualValue the model text that was in the text field before this call;
     *                            used to decide whether to preserve the selection
     * @throws IllegalArgumentException if {@code models} is empty
     */
    public void applyModelList(List<String> models, String previousManualValue) {
        Objects.requireNonNull(models, "models must not be null");
        if (models.isEmpty()) {
            throw new IllegalArgumentException("models must not be empty");
        }

        comboBox.getItems().setAll(models);

        // Preserve the previous value only when it appears in the new list.
        String previous = previousManualValue == null ? "" : previousManualValue;
        if (!previous.isBlank() && models.contains(previous)) {
            comboBox.setValue(previous);
        } else {
            comboBox.setValue(models.get(0));
        }

        currentSource = GuiModelSource.LIST_REMOTE_SUCCESS;
        applyVisibility(true);
        // Notify the callback about the newly selected value.
        String selected = comboBox.getValue();
        if (selected != null) {
            onModelChange.accept(selected);
        }
    }

    /**
     * Switches to the manual text field with the given fallback source state.
     * <p>
     * The text field retains whatever value it currently holds (or the value set programmatically
     * via {@link #setTextFieldValue(String)}). Must be called on the JavaFX Application Thread.
     *
     * @param source the non-success source state; must not be {@link GuiModelSource#LIST_REMOTE_SUCCESS}
     */
    public void applyManualFallback(GuiModelSource source) {
        Objects.requireNonNull(source, "source must not be null");
        if (source == GuiModelSource.LIST_REMOTE_SUCCESS) {
            throw new IllegalArgumentException(
                    "applyManualFallback must not be called with LIST_REMOTE_SUCCESS");
        }
        currentSource = source;
        applyVisibility(false);
    }

    /**
     * Programmatically sets the text field value without triggering the change callback.
     * <p>
     * Useful for restoring a saved model value after a provider switch. Must be called on the
     * JavaFX Application Thread.
     *
     * @param value the new text field value; {@code null} is treated as an empty string
     */
    public void setTextFieldValue(String value) {
        programmaticTextFieldSet = true;
        try {
            textField.setText(value == null ? "" : value);
        } finally {
            programmaticTextFieldSet = false;
        }
    }

    /**
     * Setzt einen Tooltip mit einheitlicher Anzeigeverzögerung auf beide internen Controls
     * (ComboBox und TextField). Damit erscheint der Tooltip unabhängig davon, welches der
     * beiden Controls gerade sichtbar ist.
     * <p>
     * Darf nur auf dem JavaFX Application Thread aufgerufen werden.
     *
     * @param tooltipText der anzuzeigende Tooltip-Text; darf nicht leer sein
     */
    public void applyTooltip(String tooltipText) {
        Objects.requireNonNull(tooltipText, "tooltipText darf nicht null sein");
        Tooltip comboTooltip = new Tooltip(tooltipText);
        comboTooltip.setShowDelay(Duration.millis(300));
        comboBox.setTooltip(comboTooltip);

        Tooltip textTooltip = new Tooltip(tooltipText);
        textTooltip.setShowDelay(Duration.millis(300));
        textField.setTooltip(textTooltip);
    }

    /**
     * Returns the JavaFX node that represents this container and can be added to the scene graph.
     *
     * @return {@code this} container; never {@code null}
     */
    public Node asNode() {
        return this;
    }

    /**
     * Applies visibility to the ComboBox and TextField based on whether the list is active.
     *
     * @param listActive {@code true} to show the ComboBox, {@code false} to show the TextField
     */
    private void applyVisibility(boolean listActive) {
        comboBox.setVisible(listActive);
        comboBox.setManaged(listActive);
        textField.setVisible(!listActive);
        textField.setManaged(!listActive);
    }
}
