package application.customfxwidgets.topicconfig;

import application.customfxwidgets.CustomFxWidgetsLoader;
import application.customfxwidgets.Displayable;
import application.customfxwidgets.TopicConfigComboBoxConfigurator;
import application.displaybehaviour.DetachableDisplayBehaviour;
import application.displaybehaviour.DisplayBehaviour;
import application.displaybehaviour.ModelConfigObjectsGuiInformer;
import application.kafka.cluster.ClusterStatusChecker;
import application.kafka.cluster.KafkaClusterProxies;
import application.kafka.cluster.KafkaClusterProxy;
import application.kafka.cluster.TriStateConfigEntryValue;
import application.kafka.dto.TopicAggregatedSummary;
import application.logging.Logger;
import application.model.modelobjects.KafkaBrokerConfig;
import application.model.modelobjects.KafkaTopicConfig;
import application.utils.GuiUtils;
import application.utils.ConfigNameGenerator;
import application.utils.TooltipCreator;
import application.utils.ValidatorUtils;
import application.utils.kafka.KafkaBrokerHostInfo;
import com.sun.javafx.scene.control.skin.TextFieldSkin;
import impl.org.controlsfx.autocompletion.SuggestionProvider;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.AnchorPane;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.textfield.AutoCompletionBinding;
import org.controlsfx.control.textfield.TextFields;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;


public class TopicConfigView extends AnchorPane implements Displayable {
    private static final String FXML_FILE = "TopicConfigView.fxml";
    private static final PseudoClass OK_TOPIC_EXISTS_PSEUDO_CLASS = PseudoClass.getPseudoClass("ok_topic_exists");
    private static final PseudoClass TOPIC_WILL_BE_AUTOCREATED_PSEUDO_CLASS = PseudoClass.getPseudoClass("warn_topic_will_be_autocreated");
    private static final PseudoClass CANNOT_USE_TOPIC_PSEUDO_CLASS = PseudoClass.getPseudoClass("error_cannot_use_topic");
    private final KafkaTopicConfig config;
    private final DisplayBehaviour displayBehaviour;
    private final ClusterStatusChecker statusChecker;
    private final TopicConfigComboBoxConfigurator<KafkaBrokerConfig> comboBoxConfigurator;
    private final SuggestionProvider<String> suggestionProvider = SuggestionProvider.create(Collections.emptyList());
    private AutoCompletionBinding<String> stringAutoCompletionBinding;
    private KafkaClusterProxies kafkaClusterProxies;
    @FXML
    private ComboBox<KafkaBrokerConfig> kafkaBrokerComboBox;
    @FXML
    private TextField topicConfigNameField;
    @FXML
    private TextField topicNameField;
    @FXML
    private Button connectionCheckButton;
    @FXML
    private ToggleButton detachPaneButton;
    private Runnable refreshCallback;
    private final MenuItem generateNameMenuItem = new MenuItem("Generate name");

    public TopicConfigView(KafkaTopicConfig config,
                           AnchorPane parentPane,
                           ModelConfigObjectsGuiInformer guiInformer,
                           Runnable refreshCallback,
                           ObservableList<KafkaBrokerConfig> brokerConfigs,
                           ClusterStatusChecker statusChecker,
                           KafkaClusterProxies kafkaClusterProxies) throws IOException {
        this.statusChecker = statusChecker;
        this.kafkaClusterProxies = kafkaClusterProxies;

        CustomFxWidgetsLoader.loadAnchorPane(this, FXML_FILE);

        this.refreshCallback = refreshCallback;
        this.config = config;

        StringExpression windowTitle = composeBrokerConfigWindowTitle();
        displayBehaviour = new DetachableDisplayBehaviour(parentPane,
                windowTitle,
                this,
                detachPaneButton.selectedProperty(),
                config,
                guiInformer);

        configureComboBox(brokerConfigs);
        GuiUtils.configureComboBoxToClearSelectedValueIfItsPreviousValueWasRemoved(kafkaBrokerComboBox);
        comboBoxConfigurator = new TopicConfigComboBoxConfigurator<>(kafkaBrokerComboBox, config);
        comboBoxConfigurator.configure();
        configureTopicConfigNameField();
        configureTopicNameField();
        configureConnectionCheckButton();

        setCallbackToUpdateTopicNameAppearanceWhenTopicNameChanges();
        setCallbackForBrokerConfigChanges();
        resetBrokerConfigToFireUpAllCallbacksForTheFirstTimeToSetupControls();

    }

    @Override
    public void display() {
        displayBehaviour.display();
    }

    @FXML
    private void initialize() {
        setupSuggestionProviderForTextField();
        configureNameGenerator();
        addAdditionalEntryToConfigNameContextMenu();
    }

    private void configureNameGenerator() {
        //saveToFilePopupMenuItem.setOnAction(event -> toFileSaver.saveContentToFile());
        generateNameMenuItem.setOnAction(event->{
            final String newName = ConfigNameGenerator.generateNewTopicConfigName(config);
            topicConfigNameField.setText(newName);
        });
    }

    private void addAdditionalEntryToConfigNameContextMenu() {
        TextFieldSkin customContextSkin = new TextFieldSkin(topicConfigNameField) {
            @Override
            public void populateContextMenu(ContextMenu contextMenu) {
                super.populateContextMenu(contextMenu);
                contextMenu.getItems().add(0, new SeparatorMenuItem());
                contextMenu.getItems().add(0, generateNameMenuItem);
            }
        };
        topicConfigNameField.setSkin(customContextSkin);

    }

    private void setupSuggestionProviderForTextField() {
        if (null == stringAutoCompletionBinding) {
            stringAutoCompletionBinding = TextFields.bindAutoCompletion(topicNameField, suggestionProvider);
            stringAutoCompletionBinding.setHideOnEscape(true);
        }
    }

    private void configureComboBox(ObservableList<KafkaBrokerConfig> brokerConfigs) {
        kafkaBrokerComboBox.setItems(brokerConfigs);
    }


    private void configureConnectionCheckButton() {
        connectionCheckButton.disableProperty().bind(Bindings.isNull(config.relatedConfigProperty()));
    }

    private StringExpression composeBrokerConfigWindowTitle() {
        return new ReadOnlyStringWrapper("Topic configuration")
                .concat(" '").concat(config.nameProperty()).concat("' (")
                .concat("topic:")
                .concat(config.topicNameProperty())
                .concat(")");
    }

    private void setCallbackToUpdateTopicNameAppearanceWhenTopicNameChanges() {
        topicNameField.textProperty().addListener((observable, oldValue, newValue) ->
                updateTopicNameTextFieldAppearance(config.getRelatedConfig()));
    }

    private void configureTopicNameField() {
        topicNameField.setText(config.getTopicName());
        GuiUtils.configureTextFieldToAcceptOnlyValidData(topicNameField,
                config::setTopicName,
                ValidatorUtils::isStringIdentifierValid,
                                                         refreshCallback);
    }

    private void resetBrokerConfigToFireUpAllCallbacksForTheFirstTimeToSetupControls() {
        final KafkaBrokerConfig c = config.getRelatedConfig();
        config.setRelatedConfig(null);
        config.setRelatedConfig(c);
    }

    private void setCallbackForBrokerConfigChanges() {

        config.relatedConfigProperty().addListener((ObservableValue<? extends KafkaBrokerConfig> observable,
                                                    KafkaBrokerConfig oldValue,
                                                    KafkaBrokerConfig newValue) -> {

            updateTopicNameTextFieldAppearance(newValue);
            resetTopicnameTextFieldAutoCompletionForNewBrokerConfig(newValue);
            setCallbackForChangingTopicNameAppearanceWhenBrokerConfigInternalPropertiesChanges(oldValue, newValue);
            setCallbackForUpdatingTopicNameTextFieldBackgroundIfCurrentClusterSummaryChanges(oldValue, newValue);
        });
    }

    private void stringPropertyCallbackForUpdatingTextFieldAppearance(ObservableValue<? extends String> ignored1,
                                                                      String ignored2,
                                                                      String ignored3) {
        updateTopicNameTextFieldAppearance(config.getRelatedConfig());
    }

    private void setCallbackForChangingTopicNameAppearanceWhenBrokerConfigInternalPropertiesChanges(KafkaBrokerConfig oldValue,
                                                                                                    KafkaBrokerConfig newValue) {

        if (oldValue != null) {
            oldValue.hostNameProperty().removeListener(this::stringPropertyCallbackForUpdatingTextFieldAppearance);
            oldValue.portProperty().removeListener(this::stringPropertyCallbackForUpdatingTextFieldAppearance);
        }
        if (newValue != null) {
            newValue.hostNameProperty().addListener(this::stringPropertyCallbackForUpdatingTextFieldAppearance);
            newValue.portProperty().addListener(this::stringPropertyCallbackForUpdatingTextFieldAppearance);
        }
    }

    private void updateTopicNameTextFieldAppearance(KafkaBrokerConfig brokerConfig) {
        if (brokerConfig == null) {
            setTopicNameTextFieldStylePropertiesBasedOnClusterConfig(null);
            resetTopicnameTextFieldAutoCompletionForClusterProxy(null);
            return;
        }
        final KafkaBrokerHostInfo hostInfo = brokerConfig.getHostInfo();
        final KafkaClusterProxy proxy = kafkaClusterProxies.get(hostInfo);
        resetTopicnameTextFieldAutoCompletionForClusterProxy(proxy);
        setTopicNameTextFieldStylePropertiesBasedOnClusterConfig(proxy);
    }

    private void setCallbackForUpdatingTopicNameTextFieldBackgroundIfCurrentClusterSummaryChanges(KafkaBrokerConfig oldValue,
                                                                                                  KafkaBrokerConfig newValue) {
        if (oldValue != null) {
            final ObjectProperty<KafkaClusterProxy> oldProxy = kafkaClusterProxies.getAsProperty(oldValue.getHostInfo());
            Logger.trace(String.format("removing listener for cluster proxy property for %s", oldValue.getHostInfo()));
            oldProxy.removeListener(this::updateTopicNameTextFieldPropertiesCallback);
        }

        if (newValue == null) {
            Logger.trace("not adding new listener for proxy property because new broker config is null");
            resetTopicnameTextFieldAutoCompletionForClusterProxy(null);
            return;
        }
        final ObjectProperty<KafkaClusterProxy> newProxy = kafkaClusterProxies.getAsProperty(newValue.getHostInfo());
        Logger.trace(String.format("adding listener for cluster proxy property for %s", newValue.getHostInfo()));
        newProxy.addListener(this::updateTopicNameTextFieldPropertiesCallback);
    }


    private void updateTopicNameTextFieldPropertiesCallback(ObservableValue<? extends KafkaClusterProxy> observable,
                                                            KafkaClusterProxy oldValue,
                                                            KafkaClusterProxy newValue) {
        Logger.trace(String.format("setting new topic name appearance for clusterProxy change listener: old %s, new %s",
                oldValue, newValue));

        setTopicNameTextFieldStylePropertiesBasedOnClusterConfig(newValue);
        resetTopicnameTextFieldAutoCompletionForClusterProxy(newValue);
    }


    private void configureTopicConfigNameField() {
        topicConfigNameField.setText(config.getName());
        GuiUtils.configureTextFieldToAcceptOnlyValidData(topicConfigNameField,
                config::setName,
                ValidatorUtils::isStringIdentifierValid,
                                                         refreshCallback);
    }

    @FXML
    private void checkButtonOnAction() {
        statusChecker.updateStatus(config.getRelatedConfig().getHostInfo(), true);
    }


    private void setTopicNameTextFieldStylePropertiesBasedOnClusterConfig(KafkaClusterProxy proxy) {

        resetTopicNameFieldProperties();
        final String topicName = topicNameField.getText();
        if (StringUtils.isBlank(topicName)) {
            return;
        }

        if (proxy == null) {
            return;
        }

        setCssStyleAndToolTip(proxy, topicName);
    }

    private void setCssStyleAndToolTip(KafkaClusterProxy proxy, String topicName) {
        String toolTipMessage;
        PseudoClass psuedoClass;

        final boolean topicExists = proxy.hasTopic(topicName);
        if (topicExists) {
            psuedoClass = OK_TOPIC_EXISTS_PSEUDO_CLASS;
            toolTipMessage = String.format("Topic '%s' exists.", topicName);
        } else {

            if (proxy.isTopicAutoCreationEnabled() != TriStateConfigEntryValue.False) {
                psuedoClass = TOPIC_WILL_BE_AUTOCREATED_PSEUDO_CLASS;
                toolTipMessage = String.format("Topic '%s' does not exist yet " +
                                "but will be auto-created by first connected consumer/publisher.",
                        topicName);
            } else {
                psuedoClass = CANNOT_USE_TOPIC_PSEUDO_CLASS;
                toolTipMessage = String.format("Topic '%s' does not exists. Must be created manually.", topicName);
            }
        }

        topicNameField.pseudoClassStateChanged(psuedoClass, true);
        topicNameField.setTooltip(TooltipCreator.createFrom(toolTipMessage));
    }

    private void resetTopicNameFieldProperties() {

        resetTopicnameTextFieldTooltip();
        resetTopicnameTextFieldBackgroundCss();
    }


    private void resetTopicnameTextFieldAutoCompletionForNewBrokerConfig(KafkaBrokerConfig brokerConfig) {

        setTopicSuggestions(Collections.emptyList());
        if (brokerConfig == null) {
            return;
        }
        final KafkaBrokerHostInfo hostInfo = brokerConfig.getHostInfo();
        final KafkaClusterProxy proxy = kafkaClusterProxies.get(hostInfo);
        resetTopicnameTextFieldAutoCompletionForClusterProxy(proxy);
    }

    private void setTopicSuggestions(List<String> possibleSuggestions) {

        if (possibleSuggestions.isEmpty()) {
            suggestionProvider.clearSuggestions();
            return;
        }

        suggestionProvider.addPossibleSuggestions(possibleSuggestions);

    }

    private void resetTopicnameTextFieldAutoCompletionForClusterProxy(KafkaClusterProxy proxy) {

        setTopicSuggestions(Collections.emptyList());

        if (proxy == null) {
            return;
        }

        final List<String> suggestions = proxy
                .getAggregatedTopicSummary()
                .stream().map(TopicAggregatedSummary::getTopicName).collect(Collectors.toList());

        setTopicSuggestions(suggestions);
    }

    private void resetTopicnameTextFieldBackgroundCss() {
        topicNameField.pseudoClassStateChanged(OK_TOPIC_EXISTS_PSEUDO_CLASS, false);
        topicNameField.pseudoClassStateChanged(TOPIC_WILL_BE_AUTOCREATED_PSEUDO_CLASS, false);
        topicNameField.pseudoClassStateChanged(CANNOT_USE_TOPIC_PSEUDO_CLASS, false);
    }

    private void resetTopicnameTextFieldTooltip() {
        final String topicName = topicNameField.getText();
        if (StringUtils.isBlank(topicName)) {
            topicNameField.setTooltip(null);
            return;
        }
        topicNameField.setTooltip(TooltipCreator.createFrom(String.format("Topic '%s' existence undetermined.", topicName)));
    }

}