package be.iminds.ilabt.jfed.experimenter_gui.slice.raw;

import be.iminds.ilabt.jfed.experiment.*;
import be.iminds.ilabt.jfed.experimenter_gui.util.BrowserUtil;
import be.iminds.ilabt.jfed.experimenter_gui.util.FileUtils;
import be.iminds.ilabt.jfed.experimenter_gui.util.SSHTerminalUtil;
import be.iminds.ilabt.jfed.experimenter_gui.util.StringConverters;
import be.iminds.ilabt.jfed.experimenter_gui.util.ui.TransformingObjectBinding;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Testbed;
import be.iminds.ilabt.jfed.highlevel.model.Slice;
import be.iminds.ilabt.jfed.highlevel.model.Sliver;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.StatusDetails;
import be.iminds.ilabt.jfed.preferences.GuiPreferenceKey;
import be.iminds.ilabt.jfed.preferences.JFedGuiPreferences;
import be.iminds.ilabt.jfed.rspec.model.BasicStringRspec;
import be.iminds.ilabt.jfed.rspec.model.GeantTestbedType;
import be.iminds.ilabt.jfed.rspec.model.IPv4;
import be.iminds.ilabt.jfed.rspec.model.ModelRspecType;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXAddressPool;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXModelRspec;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXRspecNode;
import be.iminds.ilabt.jfed.rspec.rspec_source.ManifestRspecSource;
import be.iminds.ilabt.jfed.ui.javafx.FXMLUtil;
import be.iminds.ilabt.jfed.ui.javafx.GlyphUtils;
import be.iminds.ilabt.jfed.ui.javafx.dialogs.JFDialogs;
import be.iminds.ilabt.jfed.util.ExecuteOnNotNull;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.XmlUtil;
import be.iminds.jfed.gts_highlevel.jobs.gts_parts.GtsExperimentPart;
import be.iminds.jfed.gts_highlevel.model.GtsXmlReservation;
import be.iminds.jfed.gts_highlevel.model.GtsXmlResource;
import com.github.twalcari.prettify.RTSyntaxHighlighter;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.ObjectExpression;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.FontAwesome;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * User: twalcari
 * Date: 2/6/14
 * Time: 8:21 AM
 */
public class RawSliceView extends StackPane implements ExperimentChangeListener {
    private static final Logger LOG = LoggerFactory.getLogger(RawSliceView.class);
    private static final String COMBINED_MANIFEST = "Combined Manifest RSpec";

    private final Experiment experiment;
    private final ManifestRspecSource canvasRspecSource;

    private final SSHTerminalUtil sshTerminalUtil;
    private final BrowserUtil browserUtil;
    private final JFedGuiPreferences jFedPreferences;
    private final TestbedInfoSource testbedInfoSource;
    private final StringConverter<Object> manifestComboboxStringConverter = new StringConverter<Object>() {
        @Override
        public String toString(Object object) {
            if (object instanceof Testbed)
                return ((Testbed) object).getLongName();
            if (object instanceof Server)
                return ((Server) object).getName();
            else if (object instanceof String)
                return (String) object;
            else
                throw new UnsupportedOperationException("Only String and Server/Testbed are supported");
        }

        @Override
        public Object fromString(String string) {
            throw new UnsupportedOperationException();
        }
    };


    @FXML
    private HBox initializingBox;
    @FXML
    private ScrollPane testbedsScrollPane;
    @FXML
    private VBox testbedsVBox;

    @FXML
    private TableView<Slice> sliceTableView;

    @FXML
    private TableColumn<Slice, String> sliceNameTableColumn;
    @FXML
    private TableColumn<Slice, String> sliceProjectTableColumn;
    @FXML
    private TableColumn<Slice, GeniUrn> sliceURNTableColumn;
    @FXML
    private TableColumn<Slice, Instant> sliceExpirationTimeTableColumn;


    public interface RawExperimentPart {
        String getTestbed();

        String getId();

        Instant getExpirationTime();

        ObjectExpression<Instant> expirationTimeProperty();

        StatusDetails getStatus();

        ObjectExpression<StatusDetails> statusProperty();
    }

    public static class SliverRawExperimentPart implements RawExperimentPart {

        private final Sliver sliver;

        private SliverRawExperimentPart(Sliver sliver) {
            this.sliver = sliver;
        }

        @Override
        public String getTestbed() {
            return sliver.getServer().getName();
        }

        @Override
        public String getId() {
            return sliver.getUrnString();
        }

        @Override
        public Instant getExpirationTime() {
            return sliver.getExpirationDate();
        }

        @Override
        public ReadOnlyObjectProperty<Instant> expirationTimeProperty() {
            return sliver.expirationDateProperty();
        }

        @Override
        public StatusDetails getStatus() {
            return sliver.getStatus();
        }

        @Override
        public ReadOnlyObjectProperty<StatusDetails> statusProperty() {
            return sliver.statusProperty();
        }
    }


    private class GtsRawExperimentPart implements RawExperimentPart {

        private final GeantTestbedType gtt;
        private final GtsXmlReservation reservation;

        private GtsRawExperimentPart(GeantTestbedType gtt, GtsXmlReservation reservation) {
            this.gtt = gtt;
            this.reservation = reservation;
        }


        @Override
        public String getTestbed() {
            return "[GTS] " + gtt.getName();
        }

        @Override
        public String getId() {
            return Integer.toString(reservation.getId());
        }

        @Override
        public Instant getExpirationTime() {
            return reservation.getEndTime().toInstant();
        }

        @Override
        public ObjectExpression<Instant> expirationTimeProperty() {
            return new TransformingObjectBinding<>(reservation.endTimeProperty(), ZonedDateTime::toInstant);
        }

        @Override
        public StatusDetails getStatus() {
            return convertStatus(reservation.getStatus());
        }

        @Override
        public ObjectExpression<StatusDetails> statusProperty() {
            return new TransformingObjectBinding<>(reservation.statusProperty(), RawSliceView::convertStatus);
        }
    }

    public static StatusDetails convertStatus(GtsXmlReservation.Status status) {
        switch (status) {
            case ACTIVATING:
            case RESERVING:
            case DEACTIVATING:
                return new StatusDetails(StatusDetails.SliverStatus.CHANGING);
            case ACTIVE:
                return new StatusDetails(StatusDetails.SliverStatus.READY);
            case RELEASING:
            case DEACTIVED:
            case DELETED:
                return new StatusDetails(StatusDetails.SliverStatus.UNALLOCATED);
            case RESERVED:
                return new StatusDetails(StatusDetails.SliverStatus.UNINITIALISED);
            case FAILURE:
                return new StatusDetails(StatusDetails.SliverStatus.FAIL);
            default:
                throw new RuntimeException("Unexpected status: " + status);
        }
    }


    @FXML
    private TableView<RawExperimentPart> sliversTableView;

    @FXML
    private TableColumn<RawExperimentPart, String> sliverTestbedTableColumn;

    @FXML
    private TableColumn<RawExperimentPart, String> sliverIdTableColumn;

    @FXML
    private TableColumn<RawExperimentPart, Instant> sliverExpirationTimeTableColumn;

    @FXML
    private TableColumn<RawExperimentPart, StatusDetails> sliverStatusTableColumn;

    @FXML
    private TableView<NodeWithLoginInfo> nodesTableView;

    @FXML
    private TableColumn<NodeWithLoginInfo, String> nodeNameTableColumn;

    @FXML
    private TableColumn<NodeWithLoginInfo, String> hostnameTableColumn;

    @FXML
    private TableColumn<NodeWithLoginInfo, Number> portTableColumn;

    @FXML
    private TableColumn<NodeWithLoginInfo, String> usernameTableColumn;

    @FXML
    private TableColumn<NodeWithLoginInfo, Boolean> loginTableColumn;


    @FXML
    private Label addressPoolsLabel;
    @FXML
    private TableView<AddressPoolIPv4> addressPoolsTableView;

    @FXML
    private TableColumn<AddressPoolIPv4, String> addressPoolClientIdTableColumn;

    @FXML
    private TableColumn<AddressPoolIPv4, String> addressPoolTestbedTableColumn;

    @FXML
    private TableColumn<AddressPoolIPv4, String> ipAddressTableColumn;

    @FXML
    private TableColumn<AddressPoolIPv4, String> netmaskTableColumn;


    /**
     * this combobox should only contain String of Server-objects!
     */
    @FXML
    private ComboBox<Object> manifestCombobox;
    @FXML
    private Label manifestLabel;

    @FXML
    private CodeArea manifestXmlView;


    private Slice slice = null;

    private final File prevSaveFile = null;
    private final RTSyntaxHighlighter highlighter;


    public RawSliceView(Experiment experiment, ManifestRspecSource canvasRspecSource, SSHTerminalUtil sshTerminalUtil,
                        BrowserUtil browserUtil, JFedGuiPreferences jFedPreferences,
                        TestbedInfoSource testbedInfoSource) {
        this.experiment = experiment;
        this.canvasRspecSource = canvasRspecSource;
        this.sshTerminalUtil = sshTerminalUtil;
        this.browserUtil = browserUtil;
        this.jFedPreferences = jFedPreferences;
        this.testbedInfoSource = testbedInfoSource;

        FXMLUtil.injectFXML(this);

        testbedsVBox.prefWidthProperty().bind(testbedsScrollPane.widthProperty().subtract(15));

        //initilaize slice tableview
        sliceNameTableColumn.setCellValueFactory(t -> new ReadOnlyStringWrapper(t.getValue().getName()));
        sliceProjectTableColumn.setCellValueFactory(t -> new ReadOnlyStringWrapper(t.getValue().getSubAuthorityName()));
        sliceURNTableColumn.setCellValueFactory(t -> new ReadOnlyObjectWrapper<>(t.getValue().getUrn()));
        sliceExpirationTimeTableColumn.setCellValueFactory(t -> t.getValue().expirationDateProperty());
        sliceExpirationTimeTableColumn.setCellFactory(TextFieldTableCell.forTableColumn(StringConverters.INSTANT_STRING_CONVERTER));

        DoubleBinding sliceTableWidthLeft = sliceTableView.widthProperty().subtract(2);
        sliceNameTableColumn.prefWidthProperty().bind(sliceTableWidthLeft.multiply(0.15));
        sliceProjectTableColumn.prefWidthProperty().bind(sliceTableWidthLeft.multiply(0.15));
        sliceURNTableColumn.prefWidthProperty().bind(sliceTableWidthLeft.multiply(0.5));
        sliceExpirationTimeTableColumn.prefWidthProperty().bind(sliceTableWidthLeft.multiply(0.2));


        sliceTableView.setRowFactory(param -> {
            TableRow<Slice> row = new TableRow<>();

            ContextMenu sliceUrnContextMenu = new ContextMenu();
            MenuItem copyUrnMenuItem = new MenuItem("Copy URN");
            copyUrnMenuItem.setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.COPY));
            copyUrnMenuItem.setOnAction(event -> {
                final Clipboard clipboard = Clipboard.getSystemClipboard();
                final ClipboardContent content = new ClipboardContent();
                content.putString(slice.getUrnString());
                clipboard.setContent(content);
            });
            sliceUrnContextMenu.getItems().add(copyUrnMenuItem);

            row.setContextMenu(sliceUrnContextMenu);
            return row;
        });


        //initialize slivers tableview
        sliverTestbedTableColumn.setCellValueFactory(t -> new ReadOnlyStringWrapper(t.getValue().getTestbed()));
        sliverIdTableColumn.setCellValueFactory(t -> new ReadOnlyStringWrapper(t.getValue().getId()));


        sliverExpirationTimeTableColumn.setCellValueFactory(param -> param.getValue().expirationTimeProperty());
        sliverExpirationTimeTableColumn.setCellFactory(TextFieldTableCell.forTableColumn(StringConverters.INSTANT_STRING_CONVERTER));


        sliverStatusTableColumn.setCellValueFactory(param -> param.getValue().statusProperty());
        sliverStatusTableColumn.setCellFactory(sliverSliverStatusTableColumn -> new StatusTableCell());

        sliversTableView.setRowFactory(param -> {

            TableRow<RawExperimentPart> row = new TableRow<>();

            ContextMenu sliverUrnContextMenu = new ContextMenu();
            MenuItem copySliverUrnMenuItem = new MenuItem("Copy ID");
            copySliverUrnMenuItem.setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.COPY));
            copySliverUrnMenuItem.setOnAction(event -> {
                final Clipboard clipboard = Clipboard.getSystemClipboard();
                final ClipboardContent content = new ClipboardContent();
                content.putString(row.getItem().getId());
                clipboard.setContent(content);
            });

            sliverUrnContextMenu.getItems().add(copySliverUrnMenuItem);

            row.setContextMenu(sliverUrnContextMenu);
            return row;
        });

        //set table widths
        DoubleBinding sliverTableWidthLeft =
                sliversTableView.widthProperty().subtract(2).subtract(sliverStatusTableColumn.widthProperty());


        sliverTestbedTableColumn.prefWidthProperty().bind(sliverTableWidthLeft.multiply(0.25));
        sliverIdTableColumn.prefWidthProperty().bind(sliverTableWidthLeft.multiply(0.5));
        sliverExpirationTimeTableColumn.prefWidthProperty().bind(sliverTableWidthLeft.multiply(0.25));

        //initialize nodes tableview

        nodeNameTableColumn.setCellValueFactory(t -> new ReadOnlyStringWrapper(t.getValue().getId()));

        hostnameTableColumn.setCellValueFactory(t -> new ReadOnlyStringWrapper(t.getValue().getHostname()));

        portTableColumn.setCellValueFactory(t -> new ReadOnlyObjectWrapper<>(t.getValue().getPort()));

        usernameTableColumn.setCellValueFactory(t -> new ReadOnlyStringWrapper(t.getValue().getUsername()));

        loginTableColumn.setCellValueFactory(
                p -> new SimpleBooleanProperty(p.getValue().getHostname() != null)
        );

        loginTableColumn.setCellFactory(t -> new NodeWithLoginInfoButtonTableCell());

        nodesTableView.setRowFactory(param -> {
            TableRow<NodeWithLoginInfo> row = new TableRow<>();

            ContextMenu loginContextManu = new ContextMenu();

            MenuItem copyLoginInfo = new MenuItem("Copy SSH login command");
            copyLoginInfo.setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.TERMINAL));
            copyLoginInfo.setOnAction(event -> {
                boolean success = false;

                if (row.getItem() instanceof RspecNodeWithLoginInfo) {
                    RspecNodeWithLoginInfo rspecNodeWithLoginInfo = (RspecNodeWithLoginInfo) row.getItem();
                    final ClipboardContent content = new ClipboardContent();

                    if (rspecNodeWithLoginInfo.getLoginService() != null) {
                        final BasicStringRspec.LoginService loginService = rspecNodeWithLoginInfo.getLoginService();
                        content.putString("ssh " + loginService.getUsername() + "@" + loginService.getHostname() + " -p " + loginService.getPort());
                        Clipboard.getSystemClipboard().setContent(content);
                        success = true;
                    }
                }
                if (!success)
                    new Alert(Alert.AlertType.ERROR, "No SSH info available.").showAndWait();
            });

            loginContextManu.getItems().add(copyLoginInfo);
            loginContextManu.getItems().add(new SeparatorMenuItem());

            MenuItem copyHostnameMenuItem = new MenuItem("Copy hostname");
            copyHostnameMenuItem.setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.COPY));
            copyHostnameMenuItem.setOnAction(event -> {
                if (row.getItem().getHostname() != null) {

                    final ClipboardContent content = new ClipboardContent();
                    content.putString(row.getItem().getHostname());
                    Clipboard.getSystemClipboard().setContent(content);

                } else
                    new Alert(Alert.AlertType.ERROR, "No hostname available").showAndWait();
            });

            loginContextManu.getItems().add(copyHostnameMenuItem);

            MenuItem copyUsernameMenuItem = new MenuItem("Copy username");
            copyUsernameMenuItem.setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.COPY));
            copyUsernameMenuItem.setOnAction(event -> {


                if (row.getItem().getUsername() != null) {
                    RspecNodeWithLoginInfo rspecNodeWithLoginInfo = (RspecNodeWithLoginInfo) row.getItem();

                    final ClipboardContent content = new ClipboardContent();
                    content.putString(row.getItem().getUsername());
                    Clipboard.getSystemClipboard().setContent(content);
                } else
                    new Alert(Alert.AlertType.ERROR, "No username available").showAndWait();
            });

            loginContextManu.getItems().add(copyUsernameMenuItem);

            MenuItem copyPortMenuItem = new MenuItem("Copy port");
            copyPortMenuItem.setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.COPY));
            copyPortMenuItem.setOnAction(event -> {
                if (row.getItem().getPort() != null) {
                    final ClipboardContent content = new ClipboardContent();
                    content.putString(Integer.toString(row.getItem().getPort()));
                    Clipboard.getSystemClipboard().setContent(content);
                } else
                    new Alert(Alert.AlertType.ERROR, "No port available").showAndWait();
            });

            loginContextManu.getItems().add(copyPortMenuItem);


            row.setContextMenu(loginContextManu);
            return row;

        });


        nodesTableView.prefHeightProperty().bind(Bindings.max(100,
                Bindings.size(nodesTableView.getItems()).multiply(28).add(25 /*header */ + 2 /*safety margin */)));

        //set tablecolumn widths
        nodeNameTableColumn.prefWidthProperty().bind(
                nodesTableView.widthProperty().subtract(loginTableColumn.getPrefWidth() + 2).multiply(0.3));
        hostnameTableColumn.prefWidthProperty().bind(
                nodesTableView.widthProperty().subtract(loginTableColumn.getPrefWidth() + 2).multiply(0.3));
        portTableColumn.prefWidthProperty().bind(
                nodesTableView.widthProperty().subtract(loginTableColumn.getPrefWidth() + 2).multiply(0.2));
        usernameTableColumn.prefWidthProperty().bind(
                nodesTableView.widthProperty().subtract(loginTableColumn.getPrefWidth() + 2).multiply(0.2));

        //initialize addresspools tableview
        BooleanBinding addressPoolShouldBeVisible = Bindings.isEmpty(addressPoolsTableView.getItems()).not();
        addressPoolsTableView.managedProperty().bind(addressPoolShouldBeVisible);
        addressPoolsTableView.visibleProperty().bind(addressPoolShouldBeVisible);
        addressPoolsLabel.managedProperty().bind(addressPoolShouldBeVisible);
        addressPoolsLabel.visibleProperty().bind(addressPoolShouldBeVisible);

        addressPoolClientIdTableColumn.setCellValueFactory(param -> param.getValue().getAddressPool().clientIdProperty());
        addressPoolTestbedTableColumn.setCellValueFactory(t ->
                new ReadOnlyStringWrapper(testbedInfoSource.getByUrnExact(t.getValue().getAddressPool().getComponentManagerId())
                        .getName()));
        ipAddressTableColumn.setCellValueFactory(param -> new ReadOnlyStringWrapper(param.getValue().getiPv4().getAddress()));
        netmaskTableColumn.setCellValueFactory(param -> new ReadOnlyStringWrapper(param.getValue().getiPv4().getNetmask()));

        addressPoolClientIdTableColumn.prefWidthProperty().bind(addressPoolsTableView.widthProperty().subtract(2).multiply(0.25));
        addressPoolTestbedTableColumn.prefWidthProperty().bind(addressPoolsTableView.widthProperty().subtract(2).multiply(0.25));
        ipAddressTableColumn.prefWidthProperty().bind(addressPoolsTableView.widthProperty().subtract(2).multiply(0.25));
        netmaskTableColumn.prefWidthProperty().bind(addressPoolsTableView.widthProperty().subtract(2).multiply(0.25));

        addressPoolsTableView.prefHeightProperty().bind(Bindings.max(100,
                Bindings.size(addressPoolsTableView.getItems()).multiply(28).add(25 /*header */ + 2 /*safety margin */)));

        addressPoolsTableView.setRowFactory(param -> {
            TableRow<AddressPoolIPv4> row = new TableRow<>();

            MenuItem copyAddressMenuItem = new MenuItem("Copy IP Address");
            copyAddressMenuItem.setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.COPY));
            copyAddressMenuItem.setOnAction(actionEvent -> {
                final ClipboardContent content = new ClipboardContent();
                content.putString(row.getItem().getiPv4().getAddress());
                Clipboard.getSystemClipboard().setContent(content);
            });


            MenuItem copyNetmaskMenuItem = new MenuItem("Copy Netmask");
            copyNetmaskMenuItem.setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.COPY));
            copyNetmaskMenuItem.setOnAction(actionEvent -> {
                final ClipboardContent content = new ClipboardContent();
                content.putString(row.getItem().getiPv4().getNetmask());
                Clipboard.getSystemClipboard().setContent(content);
            });

            ContextMenu contextMenu = new ContextMenu(copyAddressMenuItem, copyNetmaskMenuItem);
            row.setContextMenu(contextMenu);

            return row;
        });


        visibleProperty().addListener(observable -> {
            if (isVisible() && slice != null) {
                sliversTableView.itemsProperty().unbind();
                //sliversTableView.getItems().clear();
                updateExperimentPartsTable();
            }
        });

        manifestCombobox.setConverter(manifestComboboxStringConverter);
        manifestCombobox.getItems().add(COMBINED_MANIFEST);
        manifestCombobox.getSelectionModel().selectFirst();
        manifestCombobox.getSelectionModel().selectedItemProperty().addListener(observable -> updateManifestView());

        manifestXmlView.setParagraphGraphicFactory(LineNumberFactory.get(manifestXmlView));
        manifestXmlView.prefWidthProperty().bind(testbedsVBox.widthProperty().subtract(42));
        highlighter = new RTSyntaxHighlighter(manifestXmlView, "xml");

        experiment.addExperimentChangeListener(this);
        if (experiment.getSlice() != null) {
            this.initializeSlice(experiment.getSlice());
        }


        experiment.getParts().addListener(new WeakListChangeListener<>(experimentPartsListener));

        experiment.getParts().stream()
                .filter(experimentPart -> experimentPart instanceof GtsExperimentPart)
                .forEach(experimentPart -> registerGtsExperimentPart((GtsExperimentPart) experimentPart));

        updateExperimentPartsTable();
    }

    private final ListChangeListener<ExperimentPart> experimentPartsListener = change -> {
        while (change.next()) {
            for (ExperimentPart experimentPart : change.getAddedSubList()) {
                if (experimentPart instanceof GtsExperimentPart)
                    registerGtsExperimentPart((GtsExperimentPart) experimentPart);
            }
        }
    };
    private final InvalidationListener gtsInvalidationListener = observable -> {
        LOG.trace("Updating GTS parts");

        Platform.runLater(this::updateGtsReservations);
    };

    private void registerGtsExperimentPart(GtsExperimentPart experimentPart) {
        experimentPart.reservationProperty().addListener(new WeakInvalidationListener(gtsInvalidationListener));
        experimentPart.resourceProperty().addListener(new WeakInvalidationListener(gtsInvalidationListener));

        gtsInvalidationListener.invalidated(null); // force invalidation
    }


    private final InvalidationListener manifestInvalidationListener = observable -> {
        if (Platform.isFxApplicationThread()) {
            updateView();
        } else {
            Platform.runLater(this::updateView);
        }
    };

    private void disableInitializingView() {
        initializingBox.setVisible(false);
        initializingBox.setManaged(false);
        testbedsScrollPane.setVisible(true);
    }

    private void updateNodesTable() {
        ObservableList<NodeWithLoginInfo> nodeWithLoginInfos = FXCollections.observableArrayList();
        nodeWithLoginInfos.addAll(gtsNodesInfo);

        if (slice.getManifestRspec() != null) {

            final FXModelRspec modelRspec = slice.getManifestRspec().getModelRspec(ModelRspecType.FX);

            if (modelRspec != null) {
                for (FXRspecNode node : modelRspec.getNodes()) {
                    List<BasicStringRspec.LoginService> loginServices = slice.getManifestRspec().getNodeLoginInfo(node);
                    if (loginServices == null || loginServices.isEmpty()) {
                        nodeWithLoginInfos.add(new RspecNodeWithLoginInfo(node, null));
                    } else {
                        for (BasicStringRspec.LoginService loginService : loginServices) {
                            nodeWithLoginInfos.add(new RspecNodeWithLoginInfo(node, loginService));
                        }
                    }
                }

                FXCollections.sort(nodeWithLoginInfos, Comparator.comparing(NodeWithLoginInfo::getId)
                        .thenComparing(Comparator.nullsLast(Comparator.comparing(NodeWithLoginInfo::getHostname)))
                        .thenComparing(Comparator.nullsLast(Comparator.comparing(NodeWithLoginInfo::getPort)))
                        .thenComparing(Comparator.nullsLast(Comparator.comparing(NodeWithLoginInfo::getUsername))));
            } else {
                LOG.info("Could not retrieve FXModelRspec from slice.getManifestRspec(). Not initialized yet?");
            }
        } else {
            LOG.info("Could not retrieve manifest from slice.getManifestRspec(). Not initialized yet?");
        }

        nodesTableView.getItems().setAll(nodeWithLoginInfos);
    }

    private void updateAddressPoolsTable() {
        if (slice.getManifestRspec() == null || slice.getManifestRspec().getModelRspec(ModelRspecType.FX) == null) {
            LOG.warn("Could not retrieve FXModelRspec from slice.getManifestRspec()!");

            addressPoolsTableView.getItems().clear();
            return;
        }

        final FXModelRspec modelRspec = slice.getManifestRspec().getModelRspec(ModelRspecType.FX);

        //update addresspools
        List<AddressPoolIPv4> newAddressPoolIPv4s = new ArrayList<>();

        for (FXAddressPool addressPool : modelRspec.getAddressPools()) {
            for (IPv4 iPv4 : addressPool.getIPv4s()) {
                newAddressPoolIPv4s.add(new AddressPoolIPv4(addressPool, iPv4));
            }
        }
        addressPoolsTableView.getItems().setAll(newAddressPoolIPv4s);
    }

    private void updateView() {
        //if we don't have a manifest, we're still initializing
        if (experiment.getParts().isEmpty())
            return;

        disableInitializingView();

        updateNodesTable();
        updateAddressPoolsTable();
        //update the available manifest rspecs:

        Object oldSelection = manifestCombobox.getSelectionModel().getSelectedItem();

        List<Server> manifestAMsList = new ArrayList<>(slice.getManifestComponentManagers());
        manifestAMsList.sort(Comparator.comparing(Server::getName));

        ObservableList<Object> allManifests = FXCollections.observableArrayList();
        allManifests.add(COMBINED_MANIFEST);
        allManifests.addAll(manifestAMsList);

        manifestCombobox.setItems(allManifests);
        manifestCombobox.getSelectionModel().select(oldSelection);

        updateManifestView();
    }

    @FXML
    private void updateManifestView() {
        //if we don't have a manifest, we're still initializing
        if (slice == null || slice.getManifestRspec() == null)
            return;

        assert Platform.isFxApplicationThread();

        ManifestRspecSource manifestRspecSource = slice.getManifestRspec();

        if (!manifestCombobox.getSelectionModel().isEmpty() &&
                manifestCombobox.getSelectionModel().getSelectedItem() != COMBINED_MANIFEST) {
            assert manifestCombobox.getSelectionModel().getSelectedItem() instanceof Server;
            manifestRspecSource = slice.getSpecificManifestRspec(
                    ((Server) manifestCombobox.getSelectionModel().getSelectedItem()));
        }

        if (manifestRspecSource != null) {

            manifestXmlView.replaceText(XmlUtil.formatXmlFromString(manifestRspecSource.getRspecXmlString()));
        } else {
            manifestXmlView.replaceText("No manifest available");
        }
    }

    @FXML
    private void saveManifest() {
        //if we don't have a manifest, we're still initializing
        if (slice == null || slice.getManifestRspec() == null)
            return;

        assert Platform.isFxApplicationThread();
        ManifestRspecSource manifestRspecSource = slice.getManifestRspec();

        if (manifestCombobox.getSelectionModel().getSelectedItem() != COMBINED_MANIFEST) {
            assert manifestCombobox.getSelectionModel().getSelectedItem() instanceof Server;
            manifestRspecSource = slice.getSpecificManifestRspec(
                    ((Server) manifestCombobox.getSelectionModel().getSelectedItem()));
        }

        if (manifestRspecSource == null) {
            LOG.warn("No manifest available to save");
            return;
        }

        String selectedTitleString =
                manifestComboboxStringConverter.toString(manifestCombobox.getSelectionModel().getSelectedItem());
        String manifestXmlString = XmlUtil.formatXmlFromString(manifestRspecSource.getRspecXmlString());

        FileChooser fc = new FileChooser();
        fc.setTitle("Save Manifest RSpec");
        fc.getExtensionFilters().add(FileUtils.MANIFEST_RSPEC_EXTENSION_FILTER);
        fc.setInitialFileName(selectedTitleString + FileUtils.MANIFEST_RSPEC_EXTENSION);


        //set the default save dir
        if (System.getProperty("user.home") != null)
            fc.setInitialDirectory(new File(System.getProperty("user.home")));

        if (jFedPreferences.containsPreference(GuiPreferenceKey.PREF_RSPECFOLDER_MANIFEST)) {
            File dir = new File(jFedPreferences.getString(GuiPreferenceKey.PREF_RSPECFOLDER_MANIFEST));
            if (dir.exists()) {
                fc.setInitialDirectory(dir);
            }
        } else {
            if (jFedPreferences.containsPreference(GuiPreferenceKey.PREF_RSPECFOLDER_MANIFEST)) {
                File dir = new File(jFedPreferences.getString(GuiPreferenceKey.PREF_RSPECFOLDER_REQUEST));
                if (dir.exists()) {
                    fc.setInitialDirectory(dir);
                }
            }
        }


        if (prevSaveFile != null) {
            fc.setInitialDirectory(prevSaveFile.getParentFile());
//            fc.setInitialFileName(prevSaveFile.getName());
        }

        File saveFile = fc.showSaveDialog(this.getScene().getWindow());

        if (saveFile == null)
            return;

        //save folder in which the file was saved for future file selector dialogs
        jFedPreferences.setString(GuiPreferenceKey.PREF_RSPECFOLDER_MANIFEST,
                saveFile.getParentFile().getAbsolutePath());

        if (!saveFile.getAbsolutePath().endsWith(FileUtils.MANIFEST_RSPEC_EXTENSION))
            saveFile = new File(saveFile.getAbsolutePath() + FileUtils.MANIFEST_RSPEC_EXTENSION);

        final String fileContents = manifestXmlString;
        final File finalSaveFile = saveFile;
        final javafx.concurrent.Task<Void> saveTask = new javafx.concurrent.Task<Void>() {

            @Override
            protected Void call() throws Exception {
                updateMessage("Saving manifest in \"" + finalSaveFile.getAbsolutePath() + "\"");

                try (FileWriter writer = new FileWriter(finalSaveFile)) {
                    writer.write(fileContents);
                    writer.flush();
                    writer.close();
                } catch (final IOException ex) {
                    LOG.error("Error while saving experiment to file '" + finalSaveFile.getAbsolutePath() + "'", ex);
                    Platform.runLater(() -> JFDialogs.create()
                            .owner(RawSliceView.this)
                            .message("An error occurred while saving the rspec.")
                            .masthead("Error")
                            .title("Error")
                            .showException(ex));
                }
                updateMessage("Successfully saved experiment in \"" + finalSaveFile.getAbsolutePath() + "\"");

                return null;
            }
        };
        //saveTask.messageProperty().addListener(observable -> setStatus(saveTask.getMessage()));

        new Thread(saveTask).start();
    }

    public Slice getSlice() {
        return slice;
    }

    private void updateExperimentPartsTable() {
        List<RawExperimentPart> mergedList = new ArrayList<>();

        for (ExperimentPart experimentPart : experiment.getParts()) {
            if (experimentPart instanceof SfaExperimentPart) {
                mergedList.addAll(((SfaExperimentPart) experimentPart).getSlivers()
                        .stream()
                        .map(SliverRawExperimentPart::new)
                        .collect(Collectors.toList()));
            }

            if (experimentPart instanceof GtsExperimentPart) {
                GtsExperimentPart gtsExperimentPart = (GtsExperimentPart) experimentPart;
                if (gtsExperimentPart.getReservation() != null)
                    mergedList.add(new GtsRawExperimentPart(gtsExperimentPart.getGeantTestbedType(), gtsExperimentPart.getReservation()));
                else
                    new ExecuteOnNotNull<GtsXmlReservation>(gtsExperimentPart.reservationProperty()) {
                        @Override
                        public void run() {
                            Platform.runLater(() -> updateExperimentPartsTable());
                        }
                    };
            }
        }

        mergedList.sort(Comparator.comparing(RawExperimentPart::getTestbed)
                .thenComparing(RawExperimentPart::getId));

        sliversTableView.getItems().setAll(mergedList);

    }

    private void initializeSlice(Slice slice) {
        // a slice can only be set one time, after  that, the field becomes immutable
        assert (this.slice == null);
        this.slice = slice;

        LOG.debug("Initializing RawSliceView with slice {}", slice.getUrn());

        Platform.runLater(() -> {
            sliceTableView.getItems().add(slice);

            slice.getSlivers().addListener((InvalidationListener) observable -> updateExperimentPartsTable());
            updateExperimentPartsTable();

            sliversTableView.prefHeightProperty().bind(Bindings.max(100,
                    Bindings.size(sliversTableView.getItems()).multiply(25).add(25 /*header*/ + 2 /* safety margin */)));

            slice.manifestRspecProperty().addListener(new WeakInvalidationListener(manifestInvalidationListener));
            updateView();
        });
    }

    @Override
    public void onExperimentStateChange(ExperimentState newState) {
        if (this.slice == null && newState != ExperimentState.PENDING) {
            assert (experiment.getSlice() != null);
            initializeSlice(experiment.getSlice());
        }
    }

    @Override
    public void onExperimentPartAdded(be.iminds.ilabt.jfed.experiment.ExperimentPart experimentPart) {
        Platform.runLater(() -> {
            updateView();
            updateExperimentPartsTable();
            updateGtsReservations();
        });

    }

    private List<GtsNodeWithLoginInfo> gtsNodesInfo = Collections.emptyList();

    private void updateGtsReservations() {
        List<GtsNodeWithLoginInfo> newGtsNodeInfoList = new ArrayList<>();
        for (be.iminds.ilabt.jfed.experiment.ExperimentPart experimentPart : experiment.getParts()) {
            if (experimentPart instanceof GtsExperimentPart) {
                GtsExperimentPart gtsExperimentPart = (GtsExperimentPart) experimentPart;

                if (gtsExperimentPart.getReservation() != null)
                    processNodesInReservation(gtsExperimentPart, newGtsNodeInfoList);

            }
        }

        gtsNodesInfo = newGtsNodeInfoList;
        updateNodesTable();
    }

    private void processNodesInReservation(GtsExperimentPart experimentPart, List<GtsNodeWithLoginInfo> result) {
        LOG.debug("Processing nodes in reservation {}", experimentPart.getName());
        experimentPart.getResourcesInReservation().stream()
                .filter(resource -> resource.getType() == GtsXmlResource.Type.Host)
                .map(resource -> new GtsNodeWithLoginInfo(experimentPart.getGeantTestbedType(), resource))
                .forEach(result::add);
    }


    private static class StatusTableCell extends TableCell<RawExperimentPart, StatusDetails> {
        private static String statusToStyleClass(StatusDetails status) {
            return statusToStyleClass(status.getGlobalStatus());
        }

        private static String statusToStyleClass(StatusDetails.SliverStatus status) {
            switch (status) {
                case READY:
                    return "testbed-status-ready";

                case CHANGING:
                    return "testbed-status-changing";

                case FAIL:
                    return "testbed-status-fail";

                case UNALLOCATED:
                    return "testbed-status-unallocated";

                case UNINITIALISED:
                    return "testbed-status-uninitialised";

                case UNKNOWN:
                    return "testbed-status-unknown";
                default:
                    throw new IllegalArgumentException();
            }
        }

        @Override
        protected void updateItem(StatusDetails item, boolean empty) {
            super.updateItem(item, empty);

            if (LOG.isTraceEnabled())
                LOG.trace("Update sliver status with {} and {}", item, empty);

            for (StatusDetails.SliverStatus s : StatusDetails.SliverStatus.values()) {
                getStyleClass().remove(statusToStyleClass(s));
            }

            if (empty) {
                setText("");
            } else {
                setText(item.getGlobalStatus().name());
                getStyleClass().add(statusToStyleClass(item));
            }
        }
    }

    private interface NodeWithLoginInfo {
        String getId();

        String getHostname();

        Integer getPort();

        String getUsername();
    }


    private static class RspecNodeWithLoginInfo implements NodeWithLoginInfo {
        private final FXRspecNode rspecNode;
        private final BasicStringRspec.LoginService loginService;

        private RspecNodeWithLoginInfo(FXRspecNode rspecNode, BasicStringRspec.LoginService loginService) {
            this.rspecNode = rspecNode;
            this.loginService = loginService;
        }

        public String getId() {
            return rspecNode.getClientId();
        }

        @Override
        public String getHostname() {
            return loginService != null ? loginService.getHostname() : null;
        }

        @Override
        public Integer getPort() {
            return loginService != null ? loginService.getPort() : null;
        }

        @Override
        public String getUsername() {
            return loginService != null ? loginService.getUsername() : null;
        }

        public FXRspecNode getRspecNode() {
            return rspecNode;
        }

        public BasicStringRspec.LoginService getLoginService() {
            return loginService;
        }
    }

    public static class GtsNodeWithLoginInfo implements NodeWithLoginInfo {
        private final GeantTestbedType gtt;
        private final GtsXmlResource resource;

        private final ObjectProperty<URL> vncUrl = new SimpleObjectProperty<>();

        private GtsNodeWithLoginInfo(GeantTestbedType gtt, GtsXmlResource resource) {
            this.gtt = gtt;
            this.resource = resource;

            resource.capabilitiesProperty().addListener((javafx.beans.Observable observable) -> updateVncUrl());
            updateVncUrl();
        }

        private void updateVncUrl() {
            URL newVncUrl = null;

            if (resource.getCapabilities() != null && resource.getCapabilities().containsKey("console")) {

                try {
                    newVncUrl = new URL(resource.getCapabilities().get("console"));
                    LOG.info("Found login-url {} for GTS resource {} (providerId: {})",
                            newVncUrl, resource.getId(), resource.getProviderId());
                } catch (MalformedURLException e) {
                    LOG.warn("Got invalid URL {} while processing GTS resource {}",
                            resource.getCapabilities().get("console"), resource.getProviderId(), e);
                }
            } else {
                LOG.info("No login-url found for GTS resource {} (providerId: {})",
                        resource.getId(), resource.getProviderId());
            }
            this.vncUrl.setValue(newVncUrl);
        }

        @Override
        public String getId() {
            return "[GTS " + gtt.getName() + "] " + resource.getId();
        }

        @Override
        public String getHostname() {
            return null;
        }

        @Override
        public Integer getPort() {
            return null;
        }

        @Override
        public String getUsername() {
            return null;
        }

        public URL getVncUrl() {
            return vncUrl.get();
        }

        public ObjectProperty<URL> vncUrlProperty() {
            return vncUrl;
        }
    }

    private class NodeWithLoginInfoButtonTableCell extends TableCell<NodeWithLoginInfo, Boolean> {

        private final Button loginButton;

        public NodeWithLoginInfoButtonTableCell() {
            loginButton = new Button("Login");

            loginButton.setGraphic(GlyphUtils.createSmallGlyph(FontAwesome.Glyph.TERMINAL, Color.BLACK));
            setGraphic(loginButton);
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        }

        @Override
        protected void updateItem(Boolean item, boolean empty) {
            if (!empty) {
                loginButton.disableProperty().unbind();

                if (getTableRow().getItem() instanceof RspecNodeWithLoginInfo) {
                    RspecNodeWithLoginInfo rspecNodeWithLoginInfo =
                            (RspecNodeWithLoginInfo) getTableRow().getItem();

                    loginButton.setDisable(!item);
                    loginButton.setOnAction(actionEvent -> sshTerminalUtil.launchSSHTerminal(rspecNodeWithLoginInfo.getLoginService()));
                } else if (getTableRow().getItem() instanceof GtsNodeWithLoginInfo) {
                    GtsNodeWithLoginInfo gtsNodeWithLoginInfo = (GtsNodeWithLoginInfo) getTableRow().getItem();

                    loginButton.disableProperty().bind(gtsNodeWithLoginInfo.vncUrlProperty().isNull());
                    loginButton.setOnAction(actionEvent -> browserUtil.openUrlInBrowser(gtsNodeWithLoginInfo.getVncUrl()));
                }

                loginButton.setVisible(true);
            } else {
                loginButton.setVisible(false);
            }
        }
    }

    private static class AddressPoolIPv4 {
        private final FXAddressPool addressPool;
        private final IPv4 iPv4;

        public AddressPoolIPv4(FXAddressPool addressPool, IPv4 iPv4) {
            this.addressPool = addressPool;
            this.iPv4 = iPv4;
        }

        public FXAddressPool getAddressPool() {
            return addressPool;
        }

        public IPv4 getiPv4() {
            return iPv4;
        }
    }
}
