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

import be.iminds.ilabt.jfed.experimenter_gui.TriggeredMessageHandler;
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.ModelRspecEditor;
import be.iminds.ilabt.jfed.experimenter_gui.editor.properties.map.MapDialogFactory;
import be.iminds.ilabt.jfed.experimenter_gui.ui.HealthStatusPane;
import be.iminds.ilabt.jfed.experimenter_gui.ui.ResourceCell;
import be.iminds.ilabt.jfed.experimenter_gui.util.StringConverters;
import be.iminds.ilabt.jfed.fedmon.webapi.client.FedmonWebApiClient;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.*;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;
import be.iminds.ilabt.jfed.rspec.model.DiskImage;
import be.iminds.ilabt.jfed.rspec.model.SliverType;
import be.iminds.ilabt.jfed.rspec.model.SliverTypeBuilder;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXRspecInterface;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXRspecLink;
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 com.google.common.collect.Lists;
import javafx.application.Platform;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.*;
import javafx.beans.value.ObservableObjectValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Provider;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * User: twalcari
 * Date: 4/10/2014
 * Time: 11:55
 */
public class AdaptivePropertiesDialog extends BorderPane {
    private static final Logger LOG = LoggerFactory.getLogger(AdaptivePropertiesDialog.class);

    private final JFedGuiConfig config;
    private final AdvertisementFetchManager advertisementFetchManager;
    private final TestbedInfoSource testbedInfoSource;
    private final AuthorityFinder authorityFinder;

    private final ResourceClass resourceClass;

    private final FXRspecNode rspecNode;
    private final ModelRspecEditor modelRspecEditor;

    private final List<AdaptivePropertiesTab> availableTabs;
    @FXML
    private HealthStatusPane healthStatusPane;
    @FXML
    private VBox propertiesBox;

    @FXML
    private Pane diskImageBox;
    @FXML
    private Button addDiskImageButton;

    @FXML
    private TextField nameTextField;

    @FXML
    private Button saveButton;
    @FXML
    private Button cancelButton;
    @FXML
    private ComboBox<Server> resourceComboBox;
    @FXML
    private Button testbedsMapButton;

    @FXML
    private Label testbedHealthInfoLabel;

    @FXML
    private TextField diskImageTextField;

    @FXML
    private TabPane tabPane;

    private final Map<Server, Resource> serverToResourceMap;

    private final Map<Resource, ComponentManagerPropertiesPane> cmiPropertiesPanes = new HashMap<>();
    private final ObjectProperty<ComponentManagerPropertiesPane> currentCmip = new SimpleObjectProperty<>(null);
    private final BooleanProperty isSpecificNodeChosen = new SimpleBooleanProperty();
    private final ReadOnlyObjectProperty<Server> currentServer;
    private final ObservableObjectValue<Resource> currentResource;
    private final MapDialogFactory mapDialogFactory;
    private final TriggeredMessageHandler triggeredMessageHandler;
    private final Provider<SelectDiskImageDialog> selectDiskImageDialogProvider;

    /* 1 thead that fetches ServerGlimpse in the background whenever needed. */
    private final ExecutorService serverGlimpseFetchExecutor = Executors.newFixedThreadPool(1);
    private final FedmonWebApiClient fedmonWebApiClient;

    AdaptivePropertiesDialog(ModelRspecEditor modelRspecEditor, final FXRspecNode rspecNode, JFedGuiConfig config, AdvertisementFetchManager advertisementFetchManager,
                             TestbedInfoSource testbedInfoSource, AuthorityFinder authorityFinder, ResourceClass resourceClass, MapDialogFactory mapDialogFactory,
                             FedmonWebApiClient fedmonWebApiClient, TriggeredMessageHandler triggeredMessageHandler,
                             Provider<SelectDiskImageDialog> selectDiskImageDialogProvider) {
        this.config = config;
        this.advertisementFetchManager = advertisementFetchManager;
        this.testbedInfoSource = testbedInfoSource;
        this.authorityFinder = authorityFinder;
        this.resourceClass = resourceClass;
        this.mapDialogFactory = mapDialogFactory;
        this.fedmonWebApiClient = fedmonWebApiClient;
        this.triggeredMessageHandler = triggeredMessageHandler;
        this.selectDiskImageDialogProvider = selectDiskImageDialogProvider;

        assert resourceClass != null;
        assert modelRspecEditor != null;
        assert rspecNode != null;

        this.modelRspecEditor = modelRspecEditor;
        this.rspecNode = rspecNode;


        //initialize the Scene
        FXMLUtil.injectFXML(this);

        assert nameTextField != null;
        assert resourceComboBox != null;

        //initialize fields of main tab
        nameTextField.setTooltip(
                new Tooltip("Only alphanumerical, max. " + ModelRspecEditor.MAX_NODE_NAME_LENGTH + " characters"));


        //set initial values to these of the received node
        nameTextField.setText(rspecNode.getClientId());

        healthStatusPane.setType(config.getResourceType(resourceClass));

        if (rspecNode.getResourceClassId() != null) {
            ResourceClass nodeResourceClass = config.getResourceClass(rspecNode.getResourceClassId());
            if (nodeResourceClass != null) {
                assert Objects.equals(nodeResourceClass.getId(), resourceClass.getId());
            }
        }
        rspecNode.setResourceClassId(resourceClass.getId());

        //populate tabPane
        availableTabs = new ArrayList<>();

        BootScriptsTab bootScriptsTab = new BootScriptsTab(this);
        bootScriptsTab.load(rspecNode);
        availableTabs.add(bootScriptsTab);

        ExoGeniConfigTab exoGeniConfigTab = new ExoGeniConfigTab(this);
        exoGeniConfigTab.load(rspecNode);
        availableTabs.add(exoGeniConfigTab);

        RoutableIPTab routableIPTab = new RoutableIPTab(this);
        routableIPTab.load(rspecNode);
        availableTabs.add(routableIPTab);

        XenTab xenTab = new XenTab(this);
        xenTab.load(rspecNode);
        availableTabs.add(xenTab);

        //add tab for each interface. Can only be done after nameTextField has been properly initialized!
        for (FXRspecInterface iface : rspecNode.getInterfaces()) {
            tabPane.getTabs().add(new InterfacePropertiesTab(modelRspecEditor, rspecNode, iface, nameTextField.textProperty()));
        }

        assert resourceClass.getResources() != null;
        assert resourceComboBox != null;

        this.serverToResourceMap =
                resourceClass.getResources().stream()
                        .filter(AdaptivePropertiesDialog::saneResource)
                        .collect(Collectors.toMap(Resource::getServer, Function.identity()));

        serverToResourceMap.keySet().stream()
                .sorted(Comparator.comparing(Server::getName).thenComparing(Server::getId))
                .forEach(resourceComboBox.getItems()::add);
        resourceComboBox.setConverter(StringConverters.SERVER_STRING_CONVERTER);
        resourceComboBox.setCellFactory(param -> new ResourceCell(resourceClass, config));
        resourceComboBox.setButtonCell(new ResourceCell(resourceClass, config) {

            {
                nameLabel.setTextFill(Color.BLACK);
                container.setPadding(new Insets(0, 0, 0, 0));
            }

        });

        this.currentCmip.addListener((observable, oldValue, newValue) -> {
            isSpecificNodeChosen.unbind();
            if (newValue != null) {
                isSpecificNodeChosen.bind(newValue.isSpecificNodeChosen());
            } else
                isSpecificNodeChosen.setValue(false);
        });


        this.currentServer = resourceComboBox.getSelectionModel().selectedItemProperty();
        this.currentServer.addListener((observable, oldValue, newValue) -> {
            if (newValue != null) {
                try {
                    Platform.runLater(() ->
                            triggeredMessageHandler.onEvent(TriggeredMessage.GuiTrigger.SELECT_SERVER, Collections.singletonList(newValue))
                    );
                } catch (Exception e) {
                    LOG.warn("Exception processing TriggeredMessage (will be ignored)", e);
                }
            }
        });

        this.currentResource = new ObjectBinding<Resource>() {
            {
                bind(currentServer);
            }

            @Override
            protected Resource computeValue() {
                return serverToResourceMap.get(currentServer.get());
            }
        };


        this.currentResource.addListener((observableValue, oldResource, newResource) -> {

            boolean activeNodesOnly = true;

            if (oldResource != null) {

                if (oldResource.getAllowDiskImage() &&
                        oldResource.getDiskImage() != null &&
                        Objects.equals(oldResource.getDiskImage(), diskImageTextField.getText())) {
                    diskImageTextField.setText("");
                }

                activeNodesOnly = cmiPropertiesPanes.get(oldResource).isActiveNodesOnly();
            }

            if (newResource != null) {
                assert newResource.getServer() != null;

                if (!cmiPropertiesPanes.containsKey(newResource)) {
                    Runnable fetchServerGlimpse = () -> {
                        try {
                            LOG.debug("Fetching ServerGlimpse in background");
                            try {
                                //Note: FedmonWebApiCachedClient caches ServerGlimpse for 10 minutes with default settings
                                assert newResource.getServer() != null;
                                assert newResource.getServer().getId() != null;
                                final Optional<ServerGlimpse> serverGlimpse =
                                        fedmonWebApiClient.getServerGlimpseByServerId(newResource.getServer().getId());
                                if (serverGlimpse.isPresent()) {
                                    LOG.debug("Got ServerGlimpse in background");
                                    if (serverGlimpse.get() != null) {
                                        Platform.runLater(() -> {
                                            config.setServerGlimpse(newResource.getServer().getId(), serverGlimpse.get());
                                            //TODO register that fetching ServerGlimpse is done (so advertisement fetch can start if needed)
                                        });
                                    } else {
                                        assert serverGlimpse.get() != null;
                                        LOG.error("ServerGlimpse never be null for server " + newResource.getServer().getId());
                                        //TODO register that fetching ServerGlimpse is done (so advertisement fetch can start if needed)
                                    }
                                } else {
                                    LOG.debug("Did NOT get ServerGlimpse in background");
                                    //TODO register that fetching ServerGlimpse is done (so advertisement fetch can start if needed)
                                }
                            } catch (FedmonWebApiClient.FedmonWebApiClientException e) {
                                LOG.error("Error fetching ServerGlimpse for server " + newResource.getServer().getId(), e);
                            }
                        } catch (Throwable t) {
                            LOG.error("Unexpected Exception fetching ServerGlimpse", t);
                            throw t;
                        }
                    };
                    serverGlimpseFetchExecutor.submit(fetchServerGlimpse);

                    cmiPropertiesPanes.put(currentResource.get(), new ComponentManagerPropertiesPane(newResource, AdaptivePropertiesDialog.this.rspecNode,
                            activeNodesOnly, AdaptivePropertiesDialog.this.config, AdaptivePropertiesDialog.this.advertisementFetchManager, mapDialogFactory));
                }
                ComponentManagerPropertiesPane cmiPropertiesPane = cmiPropertiesPanes.get(newResource);
                assert cmiPropertiesPane != null;
                propertiesBox.getChildren().add(cmiPropertiesPane);
                currentCmip.set(cmiPropertiesPane);

                if (newResource.getAllowDiskImage() &&
                        newResource.getDiskImage() != null &&
                        diskImageTextField.getText().isEmpty()) {
                    diskImageTextField.setText(newResource.getDiskImage());
                }
                if (!newResource.getAllowDiskImage()) {
//                        diskImageTextField.setText("");
                    diskImageBox.setVisible(false);
                } else {
                    diskImageBox.setVisible(true);
                }

                ServerGlimpse serverGlimpse =
                        AdaptivePropertiesDialog.this.config.getServerGlimpse(newResource.getServer());
                healthStatusPane.setServerGlimpse(serverGlimpse);
            } else
                currentCmip.set(null);

            if (oldResource != null) {
                propertiesBox.getChildren().remove(cmiPropertiesPanes.get(oldResource));
            }

            if (newResource != null) {
                //only show disk image button for instageni and emulab-testbeds
                Server server = newResource.getServer();
                boolean showDiskImageDialog =
//                            server.isServerType("instageni") || server.isServerType("emulab") ||
                        server.hasFlag(Server.Flag.featureListDiskImages);
                addDiskImageButton.setVisible(showDiskImageDialog);
            }

            determineTabsVisibility(rspecNode);
        });

        Resource resource = config.findBestResource(resourceClass, rspecNode);

        if (resource == null) {
            //no component manager: select nothing
            resourceComboBox.getSelectionModel().clearSelection();
        } else {
            //select the current testbed in the list
            resourceComboBox.getSelectionModel().select(resource.getServer());
        }

        if (!rspecNode.getSliverTypes().isEmpty()) {
            List<DiskImage> diskImages = rspecNode.getSliverTypes().get(0).getDiskImages();

            if (!diskImages.isEmpty())
                diskImageTextField.setText(diskImages.get(0).getName() != null ?
                        diskImages.get(0).getName() :
                        diskImages.get(0).getUrl());
        }

        diskImageBox.managedProperty().bind(diskImageBox.visibleProperty());
        addDiskImageButton.managedProperty().bind(addDiskImageButton.visibleProperty());

        //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")) {
            testbedsMapButton.setVisible(false);
        }

        Platform.runLater(() ->
                determineTabsVisibility(rspecNode)
        );

        healthStatusPane.setConfig(config);
    }

    @FXML
    private void showTestbedsMap() {
        Resource selectedResource =
                mapDialogFactory.showTestbedMapDialog(getScene().getWindow(), resourceClass);
        if (selectedResource != null)
            resourceComboBox.getSelectionModel().select(selectedResource.getServer());
    }

    /**
     * This function shows and hides tabs as needed. It is called by evenListeners on the testbeds- and node-comboboxes.
     */
    private void determineTabsVisibility(FXRspecNode rspecNode) {
        int insertPosition = 1;
        for (AdaptivePropertiesTab apt : availableTabs) {
            if (apt.shouldBeVisible(rspecNode)) {
                LOG.debug("Tab {} should be made visible.", apt.getClass().getSimpleName());
                if (tabPane.getTabs().contains(apt)) {
                    insertPosition++;
                } else {
                    tabPane.getTabs().add(insertPosition, apt);
                }
            } else {
                LOG.debug("Tab {} should be made invisible.", apt.getClass().getSimpleName());
                if (tabPane.getTabs().contains(apt)) {
                    tabPane.getTabs().remove(apt);
                }
            }
        }
    }

    @FXML
    @SuppressWarnings("unchecked")
    public void onSaveAction(ActionEvent actionEvent) {
        nameTextField.setText(nameTextField.getText().trim());

        if (!ModelRspecEditor.isValidNodeName(nameTextField.getText())) {
            JFDialogs.create()
                    .owner(this)
                    .message("A node name may only contain alphanumerical characters, and is maximum " + ModelRspecEditor.MAX_NODE_NAME_LENGTH + " characters long.")
                    .masthead("The entered node name is invalid.")
                    .showError();
            return;
        }

        if (currentResource.get() == null) {
            JFDialogs.create()
                    .owner(this)
                    .message("You must select an authority for this node.")
                    .masthead("No authority selected")
                    .title("No authority selected")
                    .showError();
            return;
        }


        if (!diskImageTextField.getText().isEmpty()) {
            //first remove any extra spaces that have slipped in
            diskImageTextField.setText(diskImageTextField.getText().trim().replace(" ", ""));

            String diskImageValue = diskImageTextField.getText();

            //now check if it is a valid URN or HTTP(S)-link

            if (diskImageValue.startsWith("urn")) {
                GeniUrn diskImageUrn = GeniUrn.parse(diskImageValue);

                if (diskImageUrn == null || !Objects.equals("image", diskImageUrn.getEncodedResourceType())) {
                    JFDialogs.create()
                            .owner(this)
                            .message("The provided disk image URN is invalid. Please correct the entered value.")
                            .masthead("Invalid disk image URN")
                            .showError();

                    LOG.warn("User entered invalid disk image URN '{}'", diskImageValue);

                    return;
                }

            } else {
                //we expect a valid URL instead
                try {
                    new URL(diskImageValue);
                } catch (MalformedURLException e) {
                    JFDialogs.create()
                            .owner(this)
                            .message("The provided disk image URL is invalid. Please correct the entered value.")
                            .masthead("Invalid disk image URL")
                            .showError();


                    LOG.warn("User entered invalid disk image URL '{}'", diskImageValue);

                    return;
                }
            }
        }

        ComponentManagerPropertiesPane componentManagerPropertiesPane = cmiPropertiesPanes.get(currentResource.get());

        if (!componentManagerPropertiesPane.validate())
            return;


        for (AdaptivePropertiesTab visibleTab : tabPane.getTabs().stream()
                .filter((AdaptivePropertiesTab.class)::isInstance)
                .map(AdaptivePropertiesTab.class::cast)
                .collect(Collectors.toList())) {
            String validation = visibleTab.validateConfiguration();

            if (validation != null) {
                JFDialogs.create()
                        .owner(this)
                        .message(validation)
                        .masthead("Node configuration could not be saved")
                        .showError();
                return;
            }

        }


        for (PropertiesTab visibleTab : tabPane.getTabs().stream()
                .filter((PropertiesTab.class)::isInstance)
                .map(PropertiesTab.class::cast)
                .collect(Collectors.toList())) {
            String validation = visibleTab.validateConfiguration();

            if (validation != null) {
                JFDialogs.create()
                        .owner(this)
                        .message(validation)
                        .masthead("Node configuration could not be saved")
                        .showError();
                return;
            }

        }


        FXRspecNode currentNode = this.rspecNode;

        if (!Objects.equals(nameTextField.getText(), currentNode.getClientId())) {
            modelRspecEditor.updateNodeClientId(currentNode, nameTextField.getText());
        }

        Server server = currentServer.get();
        assert server != null;
        Resource resource = currentResource.get();
        assert resource != null;

        boolean componentManagerChanged =
                !Objects.equals(server.getDefaultComponentManagerAsGeniUrn(), currentNode.getComponentManagerId());
        currentNode.setComponentManagerId(server.getDefaultComponentManagerAsGeniUrn());

        //force SliverType to an existing type on the testbed
        //NOTE: this is a good default, that gets overwritten when the ExoGeni tab is present and is saved later in this step
        if (resource.getSliverType() != null &&
                (currentNode.getSliverTypeName() == null || !Objects.equals(resource.getSliverType(), currentNode.getSliverTypeName()))
                ) {
            SliverType resourceSliverType = new SliverTypeBuilder(resource.getSliverType()).build();
//            if (resourceSliverType != null) {
            //TODO post fedmon-v3 refactor: I don't think this has any use: we could check the advertisement info, but it is ok to just trust the Resource info
//                if (!server.getSliverTypes().isEmpty() && !server.hasSliverType(resourceSliverType.getName())) {
//                    LOG.warn("Default sliver_type for " + server.getDefaultComponentManagerAsGeniUrn() + " is \"" + resourceSliverType.getName() + "\", but that sliver_type is not in ListResources. Picking an existing one instead.");
//                    resourceSliverType = null;
//                } else {
//                    LOG.warn("Default sliver_type for " + server.getDefaultComponentManagerAsGeniUrn() + " is \"" + resourceSliverType.getName() + "\", but that sliver_type is not in ListResources. However, no alternative is found in ListResources...");
//                }
//            }

            //TODO post fedmon-v3 refactor: see above, this might not be needed.
//            if (resourceSliverType == null && !server.getSliverTypes().isEmpty())
//                resourceSliverType = server.getSliverTypes().get(0); //choose first (= quite randomly)

            if (resourceSliverType != null)
                currentNode.setSliverTypeName(resourceSliverType.getName());
        }

        currentNode.setComponentId(
                componentManagerPropertiesPane.getSelectedComponentId() != null
                        ? componentManagerPropertiesPane.getSelectedComponentId()
                        : null);

        if (componentManagerPropertiesPane.getSelectedHardwareType() != null)
            currentNode.getHardwareTypes().setAll(componentManagerPropertiesPane.getSelectedHardwareType());


        if (componentManagerChanged) {
            final boolean supportsStitching =
                    server.hasFlag(Server.Flag.featureStitching);
            List<FXRspecLink> toRemove = new ArrayList<>();

            if (!server.getTestbed().getAllowLinks() && rspecNode.getInterfaces().size() > 0) {
                JFDialogs.create()
                        .owner(this)
                        .message(String.format("'%s' does not support dedicated links. Existing links to '%s' are being removed.",
                                server.getName(),
                                rspecNode.getClientId()))
                        .masthead("Illegal links detected")
                        .title("Illegal links detected")
                        .showWarning();

                toRemove.addAll(Lists.transform(rspecNode.getInterfaces(),
                        rspecInterface -> rspecInterface.isLinkBound() ? rspecInterface.getLink() : null
                ));
            } else {
                for (FXRspecInterface iface : currentNode.getInterfaces()) {
                    if (!iface.isLinkBound())
                        continue;

                    if (iface.getLink().isStitched(testbedInfoSource)) {

                        if (!isValidStitchedLink(iface.getLink())) {

                            Alert alert =
                                    new Alert(Alert.AlertType.WARNING,
                                            String.format("By changing the authority of this node, the link '%s' has become a stitched link.\n" +
                                                            "However, one or more authorities do not support stitched links. It is therefore removed.",
                                                    iface.getLink().getClientId()),
                                            ButtonType.OK);
                            alert.setHeaderText("Invalid stitched link");
                            alert.setTitle("Invalid stitched link");
                            alert.show();

                            toRemove.add(iface.getLink());
                        } else if (JFDialogs.create()
                                .owner(this)
                                .message(
                                        "Link '" + iface.getLink().getClientId() + "' is now a stitched link.\n\n" +
                                                "Stitched links between testbeds are currently VERY experimental and not supported." +
                                                " Do you want to remove this link?")
                                .masthead("Experimental functionality detected.")
                                .title("Experimental functionality detected.")
                                .buttonTypes(ButtonType.YES, ButtonType.NO)
                                .showWarning().filter(r -> r == ButtonType.YES).isPresent()) {


                            toRemove.add(iface.getLink());

                        } else {

                            modelRspecEditor.convertLinkToStitchedLink(iface.getLink());
                        }
                    } else {
                        //the link used to be stitched, but isn't anymore
                        JFDialogs.create()
                                .owner(this)
                                .message("Link '" + iface.getLink().getClientId() + "' is now a normal link." +
                                        " Some settings are reverted to default.")
                                .masthead("Link type change detected")
                                .title("Link type change detected")
                                .showInformation();

                        modelRspecEditor.convertLinkToNormalLink(iface.getLink());

                    }
                }
            }

            toRemove.forEach(modelRspecEditor.getModelRspec()::deleteLink);
        }

        if (!currentNode.getSliverTypes().isEmpty()) {
            SliverType sliverType = currentNode.getSliverTypes().get(0);

            if (sliverType != null) {
                assert diskImageTextField != null;
                if (diskImageTextField.getText() != null) {
                    String diskImageString = diskImageTextField.getText().trim();

                    if (!diskImageString.isEmpty()) {
                        currentNode.setSliverTypeDiskImage(diskImageString);
                    } else {
                        currentNode.setSliverTypeDiskImage(null);
                    }
                } else {
                    currentNode.setSliverTypeDiskImage(null);
                }
            }
        } else {
            LOG.error("Could not find sliver-type on currentNode: cannot save disk image");
        }

        //check if we need to change the node exclusivity
        Boolean exclusive =
                config.getNodeExclusivity(currentNode.getComponentManagerId(), currentNode.getSliverTypeName());

        if (exclusive != null) {
            currentNode.setExclusive(exclusive);
        }

        final FXRspecNode resultNode = currentNode;

        tabPane.getTabs().stream()
                .filter(tab -> tab instanceof AdaptivePropertiesTab)
                .map(tab -> (AdaptivePropertiesTab) tab).forEach(tab -> {
            assert (tab.shouldBeVisible(resultNode)) :
                    tab.getClass().getName() + "indicates it shouldn't be visible at this time!";

            //we assume here that when the tab is visible, it is also able to save it's information
            // to the new FXRspecNode-class!
            // THIS IS POTENTIALLY UNSAFE!
            tab.save(resultNode);
        });


        tabPane.getTabs().stream()
                .filter(tab -> tab instanceof PropertiesTab)
                .map(tab -> (PropertiesTab) tab)
                .forEach(PropertiesTab::save);


        ResourceClass resourceClass = config.findBestResourceClass(currentNode);
        if (resourceClass != null)
            currentNode.setResourceClassId(resourceClass.getId());


        Stage stage = (Stage) saveButton.getScene().getWindow();
        stage.close();
    }

    String getCurrentSliverTypeName() {
        //get the Resource for the authority that is currently visible
        Resource resource = currentResource.get();

        if (resource == null)
            return null;

        if (rspecNode.getSliverTypeName() != null &&
                resource.getSliverType() != null && Objects.equals(resource.getSliverType(), rspecNode.getSliverTypeName())
                ) {
            return rspecNode.getSliverTypeName();
        }

        //TODO: check if rspecNode.getSliverTypeName() is valid for this server and change resourceType if needed?

        //fallback onto the default sliver-type defined in the CMI
        String fallbackSliverType = resource.getSliverType();

        if (fallbackSliverType != null) {
            return fallbackSliverType;
        }

        //TODO: check if this functionality should be restored
//        LOG.warn("Default sliver_type for {} is '{}', but that sliver_type is not in ListResources. " +
//                        "Picking an existing one instead.",
//                server.getName(), fallbackSliverType);

//        if (!server.getSliverTypes().isEmpty())
//            return server.getSliverTypes().get(0).getName(); //choose first (= quite randomly)
//        else
        return null;
    }


    private boolean isValidStitchedLink(FXRspecLink link) {
        for (FXRspecInterface iface : link.getInterfaces()) {
            if (!iface.isNodeBound())
                continue;


            Server nodeAuth =
                    authorityFinder.findByUrn(iface.getNode().getComponentManagerId(), AuthorityFinder.Purpose.STITCHING);

            if (!nodeAuth.hasFlag(Server.Flag.featureStitching))
                return false;
        }

        return true;
    }


    @FXML
    public void onCancelAction(ActionEvent actionEvent) {
        Stage stage = (Stage) cancelButton.getScene().getWindow();
        stage.close();
    }

    @FXML
    private void onRequestDiskImageAction() {
        SelectDiskImageDialog dialog = selectDiskImageDialogProvider.get();
        dialog.setResource(currentResource.get());
        dialog.showAndWait().ifPresent(diskImageTextField::setText);
    }

    /**
     * @return may return null if nothing is selected
     */
    public Resource getSelectedResource() {
        return currentResource.get();
    }

    public Server getSelectedServer() {
        return currentServer.get();
    }


    public ObservableObjectValue<Resource> selectedResourceProperty() {
        return currentResource;
    }

    public ReadOnlyBooleanProperty isSpecificNodeChosenProperty() {
        return isSpecificNodeChosen;
    }

    public JFedGuiConfig getConfig() {
        return config;
    }

    public AdvertisementFetchManager getAdvertisementFetchManager() {
        return advertisementFetchManager;
    }

    public ResourceClass getResourceClass() {
        return resourceClass;
    }

    public String getResourceClassId() {
        return resourceClass == null ? null : resourceClass.getId();
    }



    public static boolean saneResource(Resource resource) {
        if (resource == null) {
            LOG.warn("Resource == null in resourceClass.getResources()");
            return false;
        }
        if (resource.getServer() == null) {
            LOG.warn("Resource.getServer() == null in resourceClass.getResources()" +
                    " for resource="+resource.getId());
            return false;
        }
        if (resource.getServer().getId() == null) {
            LOG.warn("Resource.getServer().getId() == null in resourceClass.getResources()" +
                    " for resource="+resource.getId());
            return false;
        }
        if (resource.getServer().getName() == null) {
            LOG.warn("Resource.getServer().getName() == null in resourceClass.getResources()" +
                    " for resource="+resource.getId()+
                    " server="+resource.getServer().getId());
            return false;
        }
        return true;
    }
}
