package be.iminds.ilabt.jfed.experimenter_gui.editor.properties;

import be.iminds.ilabt.jfed.experimenter_gui.config.AdvertisementFetchManager;
import be.iminds.ilabt.jfed.experimenter_gui.config.JFedGuiConfig;
import be.iminds.ilabt.jfed.experimenter_gui.editor.properties.map.ImageMap;
import be.iminds.ilabt.jfed.experimenter_gui.editor.properties.map.MapDialogFactory;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Resource;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.JFedConnectivityException;
import be.iminds.ilabt.jfed.rspec.model.HardwareType;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXRspecNode;
import be.iminds.ilabt.jfed.ui.javafx.FXMLUtil;
import be.iminds.ilabt.jfed.ui.javafx.dialogs.JFDialogs;
import be.iminds.ilabt.jfed.util.GeniUrn;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Objects;
import java.util.Optional;

/**
 * User: twalcari
 * Date: 8/29/2014
 * Time: 11:26
 */
public class ComponentManagerPropertiesPane extends StackPane {
    private final static Logger LOG = LoggerFactory.getLogger(ComponentManagerPropertiesPane.class);

    private final JFedGuiConfig config;
    private final AdvertisementFetchManager advertisementFetchManager;
    private final MapDialogFactory mapDialogFactory;
    private final Resource resource;

    @FXML
    private ComboBox<JFedGuiConfig.ComponentInfo> componentIdComboBox;
    @FXML
    private ComboBox<JFedGuiConfig.HardwareTypeInfo> hardwareTypesComboBox;
    @FXML
    private ToggleGroup selectComponentIdToggleGroup;
    @FXML
    private Label chooseNodeLabel;
    @FXML
    private Button refreshAdvertisementButton;
    @FXML
    private RadioButton componentIdNodeRadioButton, anyNodeRadioButton, hardwareTypeNodeRadioButton;
    @FXML
    private ProgressBar advertisementFetchProgressBar;
    @FXML
    private VBox advertisementFetchOverlay;
    @FXML
    private Button nodesVizButton;

    @FXML
    private CheckBox onlyAvailableNodes;


    private final ObjectProperty<AdvertisementFetchManager.Resources.RefreshAdvertisementTask> activeRefreshTask =
            new SimpleObjectProperty<>();
    private final ObjectProperty<AdvertisementFetchManager.Resources> activeResources = new SimpleObjectProperty<>();

    public ComponentManagerPropertiesPane(final Resource resource,
                                          FXRspecNode node, boolean initialAvailableNodesOnly,
                                          JFedGuiConfig config, AdvertisementFetchManager advertisementFetchManager,
                                          MapDialogFactory mapDialogFactory) {
        assert resource != null;
        assert node != null;

        this.resource = resource;
        this.config = config;
        this.advertisementFetchManager = advertisementFetchManager;
        this.mapDialogFactory = mapDialogFactory;

        //initialize FXML
        FXMLUtil.injectFXML(this);

        onlyAvailableNodes.setSelected(initialAvailableNodesOnly);

        Server server = resource.getServer();
        assert server != null : "Resource "+resource.getId()+" has no server";

        JFedGuiConfig.Scope initialScope = initialAvailableNodesOnly ? JFedGuiConfig.Scope.AVAILABLE_ONLY : JFedGuiConfig.Scope.ALL;
        if (!resource.getAllowHardwareType() && resource.getRequireHardwareType() && resource.getHardwareType() != null) {
            componentIdComboBox.setItems(
                    new FilteredList<>(
                            config.getComponentInfo(server, initialScope),
                            (ci) -> ci.hasHardwareType(resource.getHardwareType())
                    )
            );
        } else {
            componentIdComboBox.setItems(config.getComponentInfo(server, initialScope));
        }
        hardwareTypesComboBox.setItems(config.getHardwareTypes(server, initialScope));

        //1. bind refresh overlay to activeRefreshTask
        advertisementFetchOverlay.visibleProperty().unbind();
        advertisementFetchOverlay.visibleProperty().bind(activeRefreshTask.isNotNull());

        activeRefreshTask.addListener((observable, oldValue, newValue) -> {
            if (oldValue != null) {
                advertisementFetchProgressBar.progressProperty().unbind();
            }

            if (newValue != null) {
                advertisementFetchProgressBar.progressProperty().bind(newValue.progressProperty());
            }
        });

        //2. bind activeRefreshTask to activeResources
        activeResources.addListener((observable, oldValue, newValue) -> {
            activeRefreshTask.unbind();
            activeRefreshTask.bind(newValue.currentRefreshAdvertisementTaskProperty());
        });

        //3. bind activeResources
        activeResources.set(initialAvailableNodesOnly ? this.advertisementFetchManager.getAvailableResources(server)
                : this.advertisementFetchManager.getAllResources(server));

        //initialize bindings
        componentIdComboBox.disableProperty().bind(componentIdNodeRadioButton.selectedProperty().not());
        onlyAvailableNodes.disableProperty().bind(anyNodeRadioButton.selectedProperty());

        hardwareTypesComboBox.disableProperty().bind(hardwareTypeNodeRadioButton.selectedProperty().not());

        onlyAvailableNodes.selectedProperty().addListener(observable -> {
            JFedGuiConfig.Scope scope = onlyAvailableNodes.isSelected() ?
                    JFedGuiConfig.Scope.AVAILABLE_ONLY :
                    JFedGuiConfig.Scope.ALL;

            LOG.debug("Will show "+scope+" nodes in comboboxes");

            if (onlyAvailableNodes.isSelected()) {
                this.activeResources.setValue(advertisementFetchManager.getAvailableResources(server));
            } else {
                this.activeResources.setValue(advertisementFetchManager.getAllResources(server));
            }

            if (!config.hasUpToDateComponentInfo(server, scope)) {
                refreshAdvertisement();
            }

            if (!resource.getAllowHardwareType() && resource.getRequireHardwareType() && resource.getHardwareType() != null) {
                componentIdComboBox.setItems(
                        new FilteredList<>(
                                config.getComponentInfo(server, scope),
                                (ci) -> ci.hasHardwareType(resource.getHardwareType())
                        )
                );
            } else {
                componentIdComboBox.setItems(config.getComponentInfo(server, scope));
            }
            hardwareTypesComboBox.setItems(config.getHardwareTypes(server, scope));
        });

        //initialize pane with values from node
        if (node.getComponentId() != null) {
            Optional<JFedGuiConfig.ComponentInfo> componentId = componentIdComboBox.getItems().stream()
                    .filter(compInfo -> Objects.equals(node.getComponentId(), compInfo.getUrn()))
                    .findFirst();

            if (componentId.isPresent()) {
                componentIdComboBox.getSelectionModel().select(componentId.get());
                componentIdNodeRadioButton.setSelected(true);
            } else {
                //add it to the list of known componentIds in JFedGuiConfig
                JFedGuiConfig.ComponentInfo addedInfo = config.addUserComponentInfo(server.getId(), node.getComponentId());
                componentIdComboBox.getSelectionModel().select(addedInfo);
                componentIdNodeRadioButton.setSelected(true);
            }
        } else if (!node.getHardwareTypes().isEmpty()) {
            if (node.getHardwareTypes().size() > 1) {
                LOG.warn("A node can only have one hardware type in request, but we found {}. Taking first one, and ignoring the rest!", node.getHardwareTypes().size());
            }

            HardwareType userSpecifiedHardwareType = node.getHardwareTypes().get(0);

            JFedGuiConfig.HardwareTypeInfo addedHardwareTypeInfo = config.addUserHardwareTypeInfo(server.getId(), userSpecifiedHardwareType.getName(), null);
            hardwareTypesComboBox.getSelectionModel().select(addedHardwareTypeInfo);
            hardwareTypeNodeRadioButton.setSelected(true);
        }

        //add this listener AFTER  the possible addition to hardwareTypesComboBox. This to prevent an unnecessary
        //advertisement call when componentId or hardwareType selected
        //TODO: fedmon-v3: check first if this is cached/known, so that advertisement is not requested when not needed
        //                 probably need to change gui to let user know data is cached, and can be refreshed to get latest
        componentIdNodeRadioButton.selectedProperty().addListener(observable -> {
            if (componentIdNodeRadioButton.isSelected() &&
                    !activeResources.get().isComponentIdsAvailable()) {
                refreshAdvertisement();
            }
        });

        hardwareTypeNodeRadioButton.selectedProperty().addListener(observable -> {
            if (hardwareTypeNodeRadioButton.isSelected() &&
                    !activeResources.get().isComponentIdsAvailable()) {
                refreshAdvertisement();
            }
        });

        //but make sure that a refresh is done if a fixed node assignment is mandatory!
        if (this.resource.getRequireFixedNodeAssignment()) {
            anyNodeRadioButton.setDisable(true);
            componentIdNodeRadioButton.setSelected(true);
        }

        if (!this.resource.getAllowFixedNodeAssignment()) {
            assert !this.resource.getRequireFixedNodeAssignment();
            componentIdNodeRadioButton.setVisible(false);
            componentIdComboBox.setVisible(false);
            nodesVizButton.setVisible(false);

            componentIdNodeRadioButton.setSelected(false);
        }

        if (!this.resource.getAllowFixedNodeAssignment() && !this.resource.getAllowHardwareType()) {
            //if both are not allowed, remove everything related to node selection

            assert !this.resource.getRequireFixedNodeAssignment();
            assert !this.resource.getRequireHardwareType();
            anyNodeRadioButton.setVisible(false);
            chooseNodeLabel.setVisible(false);
            onlyAvailableNodes.setVisible(false);
        }

        if (!this.resource.getAllowFixedNodeAssignment() &&
                !this.resource.getAllowHardwareType() &&
                !this.resource.getAllowDiskImage()) {
            //if these 3 are not allowed, remove everything related to advertisement RSpec
            refreshAdvertisementButton.setVisible(false);
        }

        hardwareTypesComboBox.setVisible(this.resource.getAllowHardwareType());
        hardwareTypeNodeRadioButton.setVisible(this.resource.getAllowHardwareType());

        if (this.resource.getRequireHardwareType()) {
            hardwareTypeNodeRadioButton.setSelected(true); //not really wat we want?
        }



        //check if viz-button must be enabled for this testbed
        if (this.resource.getAllowFixedNodeAssignment()) {
            JFedGuiConfig.NodesVisualization nodesVisualization = config.getNodesVisualization(this.resource.getServer());
            if (nodesVisualization != null) {
                nodesVizButton.disableProperty().bind(componentIdComboBox.disableProperty());

                if (nodesVisualization == JFedGuiConfig.NodesVisualization.MAP) {
                    nodesVizButton.setOnAction(this::showNodesMap);

                    //only show maps-button if running on a 64-bit JVM (see FEDIBBTDEV-1344)
                    String arch = System.getProperty("os.arch");

                    if (Objects.equals(arch, "x86")) {
                        nodesVizButton.setVisible(false);
                    }
                } else if (nodesVisualization == JFedGuiConfig.NodesVisualization.IMAGE) {
                    nodesVizButton.setOnAction(this::showNodesImageMap);
                }
            }
        }

    }


    private void showNodesImageMap(ActionEvent actionEvent) {
        ImageMap<GeniUrn> imageMap = config.getImageMap(resource.getServer());
        assert imageMap != null;
        JFedGuiConfig.ComponentInfo selectedNode =
                mapDialogFactory.showNodeImageMapDialog(this.getScene().getWindow(), imageMap,
                        config.getComponentInfo(
                                resource.getServer(),
                                JFedGuiConfig.Scope.getScopeForAvailable(onlyAvailableNodes.isSelected())
                        ));

        if (selectedNode != null) {
            componentIdComboBox.setValue(selectedNode);
        }
    }

    private void showNodesMap(ActionEvent actionEvent) {
        JFedGuiConfig.ComponentInfo selectedNode =
                mapDialogFactory.showNodeMapDialog(this.getScene().getWindow(),
                        config.getComponentInfo(
                                resource.getServer(),
                                JFedGuiConfig.Scope.getScopeForAvailable(onlyAvailableNodes.isSelected()
                                ))
                );

        if (selectedNode != null) {
            componentIdComboBox.setValue(selectedNode);
        }
    }


    private static boolean isConnectivityException(Throwable t) {

        while (t != null) {
            if (t.getCause() instanceof JFedConnectivityException)
                return true;
            else
                t = t.getCause();
        }
        return false;
    }


    private void refreshAdvertisement() {
        final AdvertisementFetchManager.Resources.RefreshAdvertisementTask updateAdvTask =
                activeResources.get().refreshAdvertisement();

        updateAdvTask.setOnFailed(workerStateEvent -> {
            //only show messages if pane is still visible
            if (ComponentManagerPropertiesPane.this.getParent() != null) {
                if (isConnectivityException(updateAdvTask.getException())) {
                    JFDialogs.create()
                            .owner(ComponentManagerPropertiesPane.this)
                            .message(String.format("A connectivity exception occurred while fetching the advertisement RSpec from %s.\n\n" +
                                            "Verify your internet connection, and consider enabling a proxy in the jFed Preferences.",
                                    resource.getServer().getName()))
                            .masthead("Fetching node advertisement failed")
                            .title("Fetching node advertisement failed")
                            .showException(updateAdvTask.getException());


                } else {
                    JFDialogs.create()
                            .owner(ComponentManagerPropertiesPane.this)
                            .message(String.format("An error occurred while fetching the advertisement RSpec from %s.",
                                    resource.getServer().getName()))
                            .masthead("Fetching advertisement RSpec failed")
                            .title("Fetching advertisement RSpec failed")
                            .showException(updateAdvTask.getException());

                }
            }
        });
    }

    public boolean validate() {
        if (componentIdNodeRadioButton.isSelected() && componentIdComboBox.getSelectionModel().isEmpty()) {
            JFDialogs.create()
                    .owner(this)
                    .message("Please select a node from the list")
                    .masthead("No node selected")
                    .showError();
            return false;
        }

        return true;
    }

    public GeniUrn getSelectedComponentId() {
        if (!componentIdNodeRadioButton.isSelected())
            return null;

        JFedGuiConfig.ComponentInfo res = componentIdComboBox.getSelectionModel().getSelectedItem();
        return res == null ? null : res.getUrn();
    }

    public ReadOnlyBooleanProperty isSpecificNodeChosen() {
        return componentIdNodeRadioButton.selectedProperty();
    }

    public HardwareType getSelectedHardwareType() {
        //special case: forced hardwaretype
        if (resource.getRequireHardwareType() && !resource.getAllowHardwareType() && resource.getHardwareType() != null)
            return new HardwareType(resource.getHardwareType());

        if (!hardwareTypeNodeRadioButton.isSelected())
            return null;

        JFedGuiConfig.HardwareTypeInfo hardwareTypeInfo = hardwareTypesComboBox.getSelectionModel().getSelectedItem();
        String hardwareTypeName = hardwareTypeInfo == null ? null : hardwareTypeInfo.getHardwareTypeName();
        if (hardwareTypeName == null)
            return null;

        return new HardwareType(hardwareTypeName);
    }

    @FXML
    private void onRefreshAdvertisementAction() {
        refreshAdvertisement();
    }

    @FXML
    private void showTestbedMap() {
    }

    public boolean isActiveNodesOnly() {
        return onlyAvailableNodes.isSelected();
    }
}
