package be.iminds.ilabt.jfed.ui.javafx.probe_gui;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Proxy;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.ServerBuilder;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Service;
import be.iminds.ilabt.jfed.highlevel.controller.Task;
import be.iminds.ilabt.jfed.highlevel.controller.TaskExecution;
import be.iminds.ilabt.jfed.highlevel.controller.TaskThread;
import be.iminds.ilabt.jfed.highlevel.history.ApiCallHistory;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.api.*;
import be.iminds.ilabt.jfed.lowlevel.api_scripts.GetUsersInProject;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl.AutomaticAggregateManagerWrapper;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl.AutomaticUserAndSliceApiWrapper;
import be.iminds.ilabt.jfed.lowlevel.connection.*;
import be.iminds.ilabt.jfed.lowlevel.fedmon.Fedmon;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceId;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceIdParser;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceUrn;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.InvalidSshKeyInfoException;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.SshKeyInfo;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.SshKeyInfoFactory;
import be.iminds.ilabt.jfed.lowlevel.stitching.VlanRange;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.ui.javafx.choosers.ServerChooser;
import be.iminds.ilabt.jfed.ui.javafx.dialogs.JFDialogs;
import be.iminds.ilabt.jfed.ui.javafx.log_gui.LogHistoryPanel;
import be.iminds.ilabt.jfed.ui.javafx.probe_gui.command_arguments.CommandArgumentChooser;
import be.iminds.ilabt.jfed.ui.javafx.probe_gui.command_arguments.CommandArgumentChooserFactory;
import be.iminds.ilabt.jfed.ui.javafx.probe_gui.credential_manipulation.DelegatedCredentialCreatorPanelController;
import be.iminds.ilabt.jfed.ui.javafx.probe_gui.credential_manipulation.SavePemPanelController;
import be.iminds.ilabt.jfed.ui.javafx.probe_gui.credential_manipulation.SpeaksForAbacCredentialCreatorPanelController;
import be.iminds.ilabt.jfed.ui.javafx.probe_gui.credential_manipulation.SpeaksForSfaCredentialCreatorPanelController;
import be.iminds.ilabt.jfed.ui.javafx.util.ExpandableHelp;
import be.iminds.ilabt.jfed.util.*;
import javafx.beans.binding.ListBinding;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URL;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * ProbeController
 */
public class ProbeController {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(ProbeController.class);
    public static final Comparator<MethodInfo> methodInfoComparator = (m1, m2) -> {
        int m1o = m1.annotation.order();
        int m2o = m2.annotation.order();

        //first use order
        if (m1o != m2o)
            return m1o - m2o;

        //the use fallback
        return m1.name.compareTo(m2.name);
    };
    private static Stage probeStage = null;
    private final ObservableList<CommandParameterController> argumentControllers = FXCollections.observableArrayList();
    private final ObservableList<CommandParameterModel> argumentModels = FXCollections.observableArrayList();
    @FXML
    private LogHistoryPanel logPanel;
    @FXML
    private TreeView<String> apiAndMethodTreeView;
    @FXML
    private Parent commandConfigurationBox; //Box containing server selection, call button and argumentGrid
    @FXML
    private VBox speaksForSfaCredentialCreatorBox;
    @FXML
    private VBox speaksForAbacCredentialCreatorBox;
    @FXML
    private VBox delegatedCredentialCreatorBox;
    @FXML
    private VBox savePemBox;
    @FXML
    private SpeaksForSfaCredentialCreatorPanelController speaksForSfaCredentialCreatorPanelController;
    @FXML
    private SpeaksForAbacCredentialCreatorPanelController speaksForAbacCredentialCreatorPanelController;
    @FXML
    private DelegatedCredentialCreatorPanelController delegatedCredentialCreatorPanelController;
    @FXML
    private SavePemPanelController savePemPanelControllerController;
    @FXML
    private Button callButton;
    @FXML
    private CheckBox disableClientAuthenticationCheckBox;
    //////////////////////////////// selection of URL/Authority ///////////////////////////////////
    @FXML
    private Parent serverUrlSettingsBox;
    @FXML
    private RadioButton useUserRadioButton;
    @FXML
    private RadioButton userAuthorityRadioButton;
    @FXML
    private RadioButton userCustomServerRadioButton;
    @FXML
    private CheckBox useSshProxyCheckBox;
    @FXML
    private Pane proxySettingsBox;
    @FXML
    private RadioButton useAuthProxyRadioButton;
    @FXML
    private RadioButton useCustomProxyRadioButton;
    @FXML
    private TextField proxyUsername;
    @FXML
    private TextField proxyHostname;
    @FXML
    private TextField proxyPort;
    @FXML
    private TextField proxyPrivateKey;
    @FXML
    private PasswordField proxyPrivateKeyPass;
    @FXML
    private Pane proxyPrivateKeyBox;
    @FXML
    private Label proxyFeedbackLabel;
    @FXML
    private HBox userAuthChoiceBox;
    @FXML
    private ServerChooser authChooserController;
    @FXML
    private Label loggedInUserLabel;
    @FXML
    private HBox fixedServerURLBox;
    @FXML
    private VBox editableServerURLBox;
    /////////////////////////////////////////////////////////////////////////////////////
    @FXML
    private StackPane advancedStackPane;
    @FXML
    private Button advancedConnectionButton;
    @FXML
    private Parent advancedConnectionPane;
    /////////////////////////////////////////////////////////////////////////////////////
    @FXML
    private CheckBox retryWhenBusyCheckBox;
    @FXML
    private Parent autoRetryBox;

    @FXML
    private TextField maxBusyCountField;
    @FXML
    private TextField busyPauseBeforeRetryMsField;

    @FXML
    private TextField maxTimeoutsField;
    @FXML
    private TextField timeoutPauseBeforeRetryMsField;

    @FXML
    private TextField maxPossibleTemporaryErrorsField;
    @FXML
    private TextField possibleTemporaryErrorPauseBeforeRetryMsField;
    /////////////////////////////////////////////////////////////////////////////////////
    @FXML
    private TextField serverUrlField;
    @FXML
    private Label serverUrlLabel;
    @FXML
    private TextField customServerUrlField;
    @FXML
    private CheckBox ignoreSelfSignedCheckBox;
    @FXML
    private Label ignoreSelfSignedCheckLabel;
    @FXML
    private VBox commandNameBox;
    @FXML
    private Label commandNameLabel;
    @FXML
    private ExpandableHelp commandHelp;
    @FXML
    private ScrollPane argumentScrollPane;
    @FXML
    private GridPane argumentGrid;
    @FXML
    private VBox includeHeader;
    @FXML
    private VBox nameHeader;
    @FXML
    private VBox valueHeader;
    private MethodInfo currentlySelectedTreeMethod = null;
    private Map<TreeItem<String>, MethodInfo> treeItemToMethodInfo;
    private Map<TreeItem<String>, NicknamedApiInfo> treeItemToNicknamedApiInfo;
    private TreeItem<String> credentialManipulationSfaSpeaksForTreeItem;
    private TreeItem<String> credentialManipulationAbacSpeaksForTreeItem;
    private TreeItem<String> savePemTreeItem;
    private TreeItem<String> credentialManipulationDelegationTreeItem;
    private String lastProxyError = "";

    private final JavaFXLogger logger;
    private final SfaConnectionPool/*JFedConnectionProvider*/ connectionProvider;
    private final GeniUserProvider geniUserProvider;
    private final ApiCallHistory apiCallHistory;
    private final CommandArgumentChooserFactory commandArgumentChoosterFactory;
    private final TaskThread tt;
    private final JFedPreferences jFedPreferences;
    private final AutomaticAggregateManagerWrapper.AutomaticAggregateManagerWrapperFactory
            automaticAggregateManagerWrapperFactory;
    private final AutomaticUserAndSliceApiWrapper.AutomaticUserAndSliceApiWrapperFactory
            automaticUserAndSliceApiWrapperFactory;
    private final JFedPasswordManager jFedPasswordManager;

    private final TestbedInfoSource testbedInfoSource;

    @Inject
    public ProbeController(JavaFXLogger logger, SfaConnectionPool connectionProvider, GeniUserProvider geniUserProvider,
                           ApiCallHistory apiCallHistory, CommandArgumentChooserFactory commandArgumentChoosterFactory,
                           TaskThread tt, JFedPreferences jFedPreferences, TestbedInfoSource testbedInfoSource,
                           AutomaticUserAndSliceApiWrapper.AutomaticUserAndSliceApiWrapperFactory automaticUserAndSliceApiWrapperFactory,
                           AutomaticAggregateManagerWrapper.AutomaticAggregateManagerWrapperFactory automaticAggregateManagerWrapperFactory,
                           JFedPasswordManager jFedPasswordManager) {
        this.logger = logger;
        this.connectionProvider = connectionProvider;
        this.geniUserProvider = geniUserProvider;
        this.apiCallHistory = apiCallHistory;
        this.commandArgumentChoosterFactory = commandArgumentChoosterFactory;
        this.tt = tt;
        this.jFedPreferences = jFedPreferences;
        this.testbedInfoSource = testbedInfoSource;
        this.automaticUserAndSliceApiWrapperFactory = automaticUserAndSliceApiWrapperFactory;
        this.automaticAggregateManagerWrapperFactory = automaticAggregateManagerWrapperFactory;
        this.jFedPasswordManager = jFedPasswordManager;
    }

    private static List<MethodInfo> findAvailableMethods(NicknamedApiInfo apiInfo, Object api) {
        Class targetClass = api.getClass();
        List<MethodInfo> res = new ArrayList<>();

        for (Method m : targetClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(ApiMethod.class)) {
                ApiMethod am = m.getAnnotation(ApiMethod.class);
                Class[] parameterTypes = m.getParameterTypes();
                if (api instanceof AbstractApi && (parameterTypes.length == 0 || !JFedConnection.class.isAssignableFrom(parameterTypes[0])))
                    throw new RuntimeException("Method " + m + " of an AbstractApi does not have JFedConnection as first parameter but it does have @ApiMethod");
                res.add(new MethodInfo(apiInfo, m, am));
            }
        }

        res.sort(methodInfoComparator);

        return res;
    }

    public void updateServerUrlChoiceOnApiChange(NicknamedApiInfo newSelectedNicknamedApiInfo) {
        if (geniUserProvider.isUserLoggedIn() &&
                geniUserProvider.getLoggedInGeniUser() != null &&
                geniUserProvider.getLoggedInGeniUser().getUserAuthorityServer() != null) {
            loggedInUserLabel.setText("User " + geniUserProvider.getLoggedInGeniUser().getUserUrnString() + "");
            boolean wasInvisible = !useUserRadioButton.isVisible();
            useUserRadioButton.setVisible(true);
            boolean userAuthoritySpecific =
                    newSelectedNicknamedApiInfo != null
                            && (newSelectedNicknamedApiInfo.requiresUserAuth
                            || newSelectedNicknamedApiInfo.requiresUserAuthConnection);
            if (wasInvisible && userAuthoritySpecific)
                useUserRadioButton.setSelected(true);
        } else {
            useUserRadioButton.setVisible(false);
            //loggedInUserLabel will not be visible now, we update text anyway
            loggedInUserLabel.setText("No User logged in.");
        }

        updateFixedServerUrlField(newSelectedNicknamedApiInfo);
    }

    @FXML
    private void initialize() {
        assert logPanel != null;
        assert apiAndMethodTreeView != null;
        assert useUserRadioButton != null;

        //if any of these is set invisible, remove them from layout as well
        useUserRadioButton.managedProperty().bind(useUserRadioButton.visibleProperty());
        userAuthChoiceBox.managedProperty().bind(userAuthChoiceBox.visibleProperty());
        fixedServerURLBox.managedProperty().bind(fixedServerURLBox.visibleProperty());
        editableServerURLBox.managedProperty().bind(editableServerURLBox.visibleProperty());
        ignoreSelfSignedCheckLabel.managedProperty().bind(ignoreSelfSignedCheckLabel.visibleProperty());
        loggedInUserLabel.managedProperty().bind(loggedInUserLabel.visibleProperty());

        commandConfigurationBox.managedProperty().bind(commandConfigurationBox.visibleProperty());
        serverUrlSettingsBox.managedProperty().bind(serverUrlSettingsBox.visibleProperty());
        serverUrlField.managedProperty().bind(serverUrlField.visibleProperty());
        serverUrlLabel.managedProperty().bind(serverUrlLabel.visibleProperty());
        userCustomServerRadioButton.managedProperty().bind(userCustomServerRadioButton.visibleProperty());
        speaksForSfaCredentialCreatorBox.managedProperty().bind(speaksForSfaCredentialCreatorBox.visibleProperty());
        speaksForSfaCredentialCreatorBox.setVisible(false);
        speaksForAbacCredentialCreatorBox.managedProperty().bind(speaksForAbacCredentialCreatorBox.visibleProperty());
        speaksForAbacCredentialCreatorBox.setVisible(false);
        delegatedCredentialCreatorBox.managedProperty().bind(delegatedCredentialCreatorBox.visibleProperty());
        delegatedCredentialCreatorBox.setVisible(false);
        savePemBox.managedProperty().bind(savePemBox.visibleProperty());
        savePemBox.setVisible(false);

        proxySettingsBox.managedProperty().bind(proxySettingsBox.visibleProperty());
        proxyPrivateKeyBox.managedProperty().bind(proxyPrivateKeyBox.visibleProperty());
        proxySettingsBox.visibleProperty().bind(useSshProxyCheckBox.selectedProperty());

        proxyUsername.editableProperty().bind(useCustomProxyRadioButton.selectedProperty());
        proxyHostname.editableProperty().bind(useCustomProxyRadioButton.selectedProperty());
        proxyPort.editableProperty().bind(useCustomProxyRadioButton.selectedProperty());
        proxyPrivateKeyBox.visibleProperty().bind(useCustomProxyRadioButton.selectedProperty());

        //bind radiobuttons to visible regions
        userAuthChoiceBox.visibleProperty().bind(userAuthorityRadioButton.selectedProperty().or(useUserRadioButton.selectedProperty()));
        fixedServerURLBox.visibleProperty().bind(userAuthorityRadioButton.selectedProperty().or(useUserRadioButton.selectedProperty()));
        editableServerURLBox.visibleProperty().bind(userCustomServerRadioButton.selectedProperty());

        //bind other logical connections
        loggedInUserLabel.visibleProperty().bind(useUserRadioButton.selectedProperty());
        authChooserController.editableProperty().bind(userAuthorityRadioButton.selectedProperty());
        ignoreSelfSignedCheckLabel.visibleProperty().bind(ignoreSelfSignedCheckBox.selectedProperty());
        useUserRadioButton.visibleProperty().addListener((observableValue, oldIsVisible, newIsVisible) -> {
            if (!newIsVisible && useUserRadioButton.isSelected())
                userAuthorityRadioButton.selectedProperty().set(true);
        });

        //set label min max and preferred size so it fills grid
        includeHeader.setMinWidth(includeHeader.getMaxWidth());
        includeHeader.setPrefWidth(includeHeader.getMaxWidth());
        includeHeader.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);

        nameHeader.setMinWidth(nameHeader.getMaxWidth());
        nameHeader.setPrefWidth(nameHeader.getMaxWidth());
        nameHeader.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);

        valueHeader.setMinWidth(valueHeader.getMaxWidth());
        valueHeader.setPrefWidth(valueHeader.getMaxWidth());
        valueHeader.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);

        advancedConnectionPane.managedProperty().bind(advancedConnectionPane.visibleProperty());
        advancedConnectionButton.managedProperty().bind(advancedConnectionButton.visibleProperty());
        advancedConnectionPane.setVisible(false);
        advancedConnectionButton.setVisible(true);

        RetrySettings globalRetrySettings = jFedPreferences.getRetrySettings();
        maxBusyCountField.setText(globalRetrySettings.getBusyMaxRetries() + "");
        busyPauseBeforeRetryMsField.setText(globalRetrySettings.getBusyMsBeforeRetry() + "");
        maxTimeoutsField.setText(globalRetrySettings.getTimeoutMaxRetries() + "");
        timeoutPauseBeforeRetryMsField.setText(globalRetrySettings.getTimeoutMsBeforeRetry() + "");
        maxPossibleTemporaryErrorsField.setText(globalRetrySettings.getPossibleTemporaryErrorMaxRetries() + "");
        possibleTemporaryErrorPauseBeforeRetryMsField.setText(globalRetrySettings.getPossibleTemporaryErrorMsBeforeRetry() + "");

        ChangeListener<Boolean> busyfocusListener = (observableValue, aBoolean, aBoolean2) -> updateTimeouts();
        ChangeListener<String> busyTextListener = (observableValue, a, b) -> updateTimeouts();
        maxBusyCountField.focusedProperty().addListener(busyfocusListener);
        busyPauseBeforeRetryMsField.focusedProperty().addListener(busyfocusListener);
        maxTimeoutsField.focusedProperty().addListener(busyfocusListener);
        timeoutPauseBeforeRetryMsField.focusedProperty().addListener(busyfocusListener);
        maxPossibleTemporaryErrorsField.focusedProperty().addListener(busyfocusListener);
        possibleTemporaryErrorPauseBeforeRetryMsField.focusedProperty().addListener(busyfocusListener);

        maxBusyCountField.textProperty().addListener(busyTextListener);
        busyPauseBeforeRetryMsField.textProperty().addListener(busyTextListener);
        maxTimeoutsField.textProperty().addListener(busyTextListener);
        timeoutPauseBeforeRetryMsField.textProperty().addListener(busyTextListener);
        maxPossibleTemporaryErrorsField.textProperty().addListener(busyTextListener);
        possibleTemporaryErrorPauseBeforeRetryMsField.textProperty().addListener(busyTextListener);

        autoRetryBox.managedProperty().bind(autoRetryBox.visibleProperty());
        autoRetryBox.visibleProperty().bind(retryWhenBusyCheckBox.selectedProperty());

        doAllInit();
    }

    public void showAdvancedConnection() {
        advancedConnectionPane.setVisible(true);
        advancedConnectionButton.setVisible(false);
    }

    public void hideAdvancedConnection() {
        advancedConnectionPane.setVisible(false);
        advancedConnectionButton.setVisible(true);
    }

    private static final String textErrorClass = "validation_error";

    public void updateTimeouts() {
        maxBusyCountField.getStyleClass().removeAll(textErrorClass);
        busyPauseBeforeRetryMsField.getStyleClass().removeAll(textErrorClass);
        maxTimeoutsField.getStyleClass().removeAll(textErrorClass);
        timeoutPauseBeforeRetryMsField.getStyleClass().removeAll(textErrorClass);
        maxPossibleTemporaryErrorsField.getStyleClass().removeAll(textErrorClass);
        possibleTemporaryErrorPauseBeforeRetryMsField.getStyleClass().removeAll(textErrorClass);

        if (!retryWhenBusyCheckBox.isSelected()) {
            jFedPreferences.setRetrySettings(RetrySettings.getNoRetrySettings());
        } else {
            int maxBusyCount = 0;
            long busyPauseBeforeRetryMs = 0;
            int maxTimeouts = 0;
            long timeoutPauseBeforeRetryMs = 0;
            int possibleTemporaryErrorMaxRetries = 0;
            long possibleTemporaryErrorMsBeforeRetry = 0;

            try {
                maxBusyCount = Integer.parseInt(maxBusyCountField.getText());
            } catch (NumberFormatException e) {
                maxBusyCountField.getStyleClass().add(textErrorClass);
            }
            try {
                busyPauseBeforeRetryMs = Long.parseLong(busyPauseBeforeRetryMsField.getText());
            } catch (NumberFormatException e) {
                busyPauseBeforeRetryMsField.getStyleClass().add(textErrorClass);
            }
            try {
                maxTimeouts = Integer.parseInt(maxTimeoutsField.getText());
            } catch (NumberFormatException e) {
                maxTimeoutsField.getStyleClass().add(textErrorClass);
            }
            try {
                timeoutPauseBeforeRetryMs = Long.parseLong(timeoutPauseBeforeRetryMsField.getText());
            } catch (NumberFormatException e) {
                timeoutPauseBeforeRetryMsField.getStyleClass().add(textErrorClass);
            }
            try {
                possibleTemporaryErrorMaxRetries = Integer.parseInt(maxPossibleTemporaryErrorsField.getText());
            } catch (NumberFormatException e) {
                maxPossibleTemporaryErrorsField.getStyleClass().add(textErrorClass);
            }
            try {
                possibleTemporaryErrorMsBeforeRetry =
                        Long.parseLong(possibleTemporaryErrorPauseBeforeRetryMsField.getText());
            } catch (NumberFormatException e) {
                possibleTemporaryErrorPauseBeforeRetryMsField.getStyleClass().add(textErrorClass);
            }

            jFedPreferences.setRetrySettings(new RetrySettings(
                    maxTimeouts,
                    timeoutPauseBeforeRetryMs,
                    maxBusyCount,
                    busyPauseBeforeRetryMs,
                    possibleTemporaryErrorMaxRetries,
                    possibleTemporaryErrorMsBeforeRetry));
        }
    }


    //this fills in treeItemToMethodInfo and treeItemToNicknamedApiInfo
    private void initTreeViewHelper_addApis(List<NicknamedApiInfo> apis, TreeItem<String> rootItem) {
        for (NicknamedApiInfo api : apis) {
            String name = api.nickname;
            if (name == null && api.abstractApi != null)
                name = api.abstractApi.getName();
            TreeItem<String> apiItem = new TreeItem<>(name);
            rootItem.getChildren().add(apiItem);
//            apiItem.setExpanded(true);
            treeItemToNicknamedApiInfo.put(apiItem, api);

            Server dummyServer = geniUserProvider.isUserLoggedIn() ?
                    geniUserProvider.getLoggedInGeniUser().getUserAuthorityServer()
                    : new ServerBuilder().setId(-1).setName("dummy").create();

            List<MethodInfo> methods = findAvailableMethods(api, api.getAnyApi(dummyServer));
            for (MethodInfo method : methods) {
                TreeItem<String> methodItem = new TreeItem<>(method.name);
                apiItem.getChildren().add(methodItem);
                treeItemToMethodInfo.put(methodItem, method);
            }
        }
    }

    public NicknamedApiInfo currentlySelectedTreeApi() {
        return currentlySelectedTreeMethod == null ? null : currentlySelectedTreeMethod.api;
    }

    public void initTreeView() {
        TreeItem<String> rootItem = new TreeItem<>("APIs and Methods", null/*icon*/);
        rootItem.setExpanded(true);


        TreeItem<String> amApisTreeItem = new TreeItem<>("Aggregate Manager APIs");
        rootItem.getChildren().add(amApisTreeItem);
        TreeItem<String> samaApisTreeItem = new TreeItem<>("User and Slice APIs");
        rootItem.getChildren().add(samaApisTreeItem);
        TreeItem<String> otherApisTreeItem = new TreeItem<>("Other APIs");
        rootItem.getChildren().add(otherApisTreeItem);
        TreeItem<String> wrapperApisTreeItem = new TreeItem<>("API Wrappers");
        rootItem.getChildren().add(wrapperApisTreeItem);


        treeItemToMethodInfo = new HashMap<>();
        treeItemToNicknamedApiInfo = new HashMap<>();

        List<NicknamedApiInfo> apis = new ArrayList<>();
        apis.add(new NicknamedApiInfo("ProtoGeni SA", new ProtogeniSliceAuthority(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("Planetlab SFA Registry", new PlanetlabSfaRegistryInterface(logger, jFedPreferences)));

        //this also fills in treeItemToMethodInfo and treeItemToNicknamedApiInfo
        initTreeViewHelper_addApis(apis, samaApisTreeItem);


        apis = new ArrayList<>();
        apis.add(new NicknamedApiInfo("Aggregate Manager v2", new AggregateManager2(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("Aggregate Manager v3", new AggregateManager3(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("ProtoGeni Extensions", new ProtoGeniAMExtensions(logger, jFedPreferences)));

        //this also fills in treeItemToMethodInfo and treeItemToNicknamedApiInfo
        initTreeViewHelper_addApis(apis, amApisTreeItem);


        apis = new ArrayList<>();
        apis.add(new NicknamedApiInfo("ProtoGeni Cleaninghouse", new ProtoGeniClearingHouse1(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("Stitching Computation Service", new StitchingComputationService(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("FedMon", new Fedmon(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("GTS", new GtsRest(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("SLA Collector", new SlaCollector(logger, jFedPreferences)));

        //this also fills in treeItemToMethodInfo and treeItemToNicknamedApiInfo
        initTreeViewHelper_addApis(apis, otherApisTreeItem);


        apis = new ArrayList<>();
        apis.add(new NicknamedApiInfo("Automatic User And Slice ApiWrapper",
                automaticUserAndSliceApiWrapperFactory.create(),
                null /*requiredServerType*/,
                false/*requiresConnection*/,
                false/*requiresUserAuthConnection*/,
                false /*requiresAuth*/,                   //automatically uses the user authority
                false/*requiresUserAuth*/
        ));
        apis.add(new NicknamedApiInfo("Automatic AggregateManager ApiWrapper",
                null,
                null /*requiredServerType*/,
                false/*requiresConnection*/,
                false/*requiresUserAuthConnection*/,
                true /*requiresAuth*/,
                false/*requiresUserAuth*/
        ) {
            @Override
            public Object getAnyApi(@Nonnull Server auth) {
                return automaticAggregateManagerWrapperFactory.create(auth);
            }
        });
        apis.add(new NicknamedApiInfo("GetUsersInProject",
                null,
                null /*requiredServerType*/,
                false/*requiresConnection*/,
                false/*requiresUserAuthConnection*/,
                false /*requiresAuth*/,                   //automatically uses the user authority
                false/*requiresUserAuth*/
        ) {
            @Override
            public Object getAnyApi(Server auth) {
                return new GetUsersInProject(logger, automaticUserAndSliceApiWrapperFactory, geniUserProvider);
            }
        });

        initTreeViewHelper_addApis(apis, wrapperApisTreeItem);


        TreeItem<String> fedApisTreeItem = new TreeItem<>("Federation APIs");
        samaApisTreeItem.getChildren().add(fedApisTreeItem);
        TreeItem<String> fedApisV1TreeItem = new TreeItem<>("Version 1");
        fedApisTreeItem.getChildren().add(fedApisV1TreeItem);
        TreeItem<String> fedApisV2TreeItem = new TreeItem<>("Version 2");
        fedApisTreeItem.getChildren().add(fedApisV2TreeItem);

        apis = new ArrayList<>();
        apis.add(new NicknamedApiInfo("Registry", new FederationRegistryApi1(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("Member Authority", new FederationMemberAuthorityApi1(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("Slice Authority", new FederationSliceAuthorityApi1(logger, jFedPreferences)));
        //this also fills in treeItemToMethodInfo and treeItemToNicknamedApiInfo
        initTreeViewHelper_addApis(apis, fedApisV1TreeItem);

        apis = new ArrayList<>();
        apis.add(new NicknamedApiInfo("Registry", new FederationRegistryApi2(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("Member Authority", new FederationMemberAuthorityApi2(logger, jFedPreferences)));
        apis.add(new NicknamedApiInfo("Slice Authority", new FederationSliceAuthorityApi2(logger, jFedPreferences)));
        //this also fills in treeItemToMethodInfo and treeItemToNicknamedApiInfo
        initTreeViewHelper_addApis(apis, fedApisV2TreeItem);


        TreeItem<String> credentialManipulationCategoryTreeItem = new TreeItem<>("Credential Manipulation");
        rootItem.getChildren().add(credentialManipulationCategoryTreeItem);
        credentialManipulationSfaSpeaksForTreeItem = new TreeItem<>("Speaks For (SFA)");
        credentialManipulationCategoryTreeItem.getChildren().add(credentialManipulationSfaSpeaksForTreeItem);
        credentialManipulationAbacSpeaksForTreeItem = new TreeItem<>("Speaks For (ABAC)");
        credentialManipulationCategoryTreeItem.getChildren().add(credentialManipulationAbacSpeaksForTreeItem);
        credentialManipulationDelegationTreeItem = new TreeItem<>("Delegation");
        credentialManipulationCategoryTreeItem.getChildren().add(credentialManipulationDelegationTreeItem);
        savePemTreeItem = new TreeItem<>("Save User Login");
        credentialManipulationCategoryTreeItem.getChildren().add(savePemTreeItem);

        apiAndMethodTreeView.getSelectionModel().selectedItemProperty().addListener((observableValue, oldTreeItem, newTreeItem) -> {
            MethodInfo oldMethodInfo = treeItemToMethodInfo.get(oldTreeItem);
            MethodInfo newMethodInfo = treeItemToMethodInfo.get(newTreeItem);

            NicknamedApiInfo newNicknamedApiInfo = treeItemToNicknamedApiInfo.get(newTreeItem);
            if (newNicknamedApiInfo != null) {
                newTreeItem.setExpanded(!newTreeItem.isExpanded());
            }

            currentlySelectedTreeMethod = newMethodInfo;
            selectMethod(oldMethodInfo, newMethodInfo);

            updateServerUrlChoiceOnApiChange(currentlySelectedTreeApi());

            //show either commandConfigurationBox or one of the 3 credentialManipulationBoxes
            boolean hideCommand =
                    newTreeItem == credentialManipulationSfaSpeaksForTreeItem
                            || newTreeItem == credentialManipulationAbacSpeaksForTreeItem
                            || newTreeItem == credentialManipulationDelegationTreeItem
                            || newTreeItem == savePemTreeItem;
            commandConfigurationBox.setVisible(!hideCommand);
            speaksForSfaCredentialCreatorBox.setVisible(newTreeItem == credentialManipulationSfaSpeaksForTreeItem);
            speaksForAbacCredentialCreatorBox.setVisible(newTreeItem == credentialManipulationAbacSpeaksForTreeItem);
            delegatedCredentialCreatorBox.setVisible(newTreeItem == credentialManipulationDelegationTreeItem);
            savePemBox.setVisible(newTreeItem == savePemTreeItem);
        });

        apiAndMethodTreeView.setShowRoot(false);
        apiAndMethodTreeView.setRoot(rootItem);
    }

    public void updateFixedServerUrlField(NicknamedApiInfo newNicknamedApiInfo) {
        String authUrl = "NONE";
        boolean error = true;

        if (newNicknamedApiInfo != null) {
//            boolean isSA = st.getRole().equals(ServerType.GeniServerRole.PROTOGENI_SA);

            boolean userAuthoritySpecific =
                    newNicknamedApiInfo.requiresUserAuth || newNicknamedApiInfo.requiresUserAuthConnection;
            if (userAuthoritySpecific && useUserRadioButton.isSelected()) {
                if (!geniUserProvider.isUserLoggedIn()) {
                    authUrl = "No user logged in";
                    error = newNicknamedApiInfo.requiresConnection || newNicknamedApiInfo.requiresAuth;
                } else {
                    GeniUser geniUser = geniUserProvider.getLoggedInGeniUser();
                    assert geniUser != null;
                    Server userServer = geniUser.getUserAuthorityServer();
                    assert userServer != null;

                    if (newNicknamedApiInfo.requiresConnection) {
                        ApiInfo.Api api = newNicknamedApiInfo.requiredServerType;
                        Service s = ApiInfo.findService(userServer, api);
                        if (s == null) {
                            authUrl = "User Authority " + userServer.getName() + " has no URL for " + api;
                            error = true;
                        } else {
                            authUrl = s.getUrl();
                            error = false;
                        }
                    } else {
                        authUrl = null;
                        error = false;
                    }
                }
            } else {
                Server server = authChooserController.getSelectedServer();
                if (server != null) {
                    if (newNicknamedApiInfo.requiresConnection) {
                        ApiInfo.Api api = newNicknamedApiInfo.requiredServerType;
                        Service s = ApiInfo.findService(server, api);
                        if (s == null) {
                            authUrl = "Authority " + server.getName() + " has no URL for " + api;
                            error = true;
                        } else {
                            authUrl = s.getUrl();
                            error = false;
                        }
                    } else {
                        authUrl = null;
                        error = false;
                    }
                } else {
                    //error if authority or connection to authority is required
                    error = newNicknamedApiInfo.requiresConnection || newNicknamedApiInfo.requiresAuth;
                }
            }
        }

        serverUrlField.getStyleClass().remove("textFieldError");
        if (error)
            serverUrlField.getStyleClass().add("textFieldError");

        if (newNicknamedApiInfo != null && newNicknamedApiInfo.requiresConnection) {
            serverUrlField.setVisible(true);
            serverUrlLabel.setVisible(true);
            serverUrlField.setText(authUrl);
        } else {
            serverUrlField.setVisible(false);
            serverUrlLabel.setVisible(false);
        }

        serverUrlSettingsBox.setVisible(newNicknamedApiInfo != null && (newNicknamedApiInfo.requiresConnection || newNicknamedApiInfo.requiresAuth));
    }

    /**
     * init after sfaModel set, and fxml loaded. (typically after setSfaModel, but you never know)
     */
    private void doAllInit() {
        assert logPanel != null;
        assert authChooserController != null;
        assert connectionProvider != null;

        initTreeView();

        authChooserController.selectedAuthorityProperty().addListener((observableValue, oldAuthorityInfo, newAuthorityInfo) -> updateFixedServerUrlField(currentlySelectedTreeApi()));
        useUserRadioButton.selectedProperty().addListener((observableValue, oldBool, newIsSelected) -> {
            if (newIsSelected) updateFixedServerUrlField(currentlySelectedTreeApi());
            if (newIsSelected && geniUserProvider.isUserLoggedIn() &&
                    geniUserProvider.getLoggedInGeniUser() != null &&
                    geniUserProvider.getLoggedInGeniUser().getUserAuthorityServer() != null)
                authChooserController.select(geniUserProvider.getLoggedInGeniUser().getUserAuthorityServer());
        });
        userAuthorityRadioButton.selectedProperty().addListener((observableValue, oldBool, newIsSelected) -> {
            if (newIsSelected) updateFixedServerUrlField(currentlySelectedTreeApi());
        });


        ColumnConstraints colInfoInclude = new ColumnConstraints();
        ColumnConstraints colInfoName = new ColumnConstraints();
        ColumnConstraints colInfoValue = new ColumnConstraints();
        colInfoValue.setHgrow(Priority.ALWAYS);
        colInfoValue.setMaxWidth(Double.MAX_VALUE);
        colInfoInclude.setFillWidth(true);
        colInfoName.setFillWidth(true);
        colInfoValue.setFillWidth(true);

//        colInfoInclude.setPercentWidth(10);
//        colInfoName.setPercentWidth(30);
//        colInfoValue.setPercentWidth(60);

        argumentGrid.getColumnConstraints().add(colInfoInclude);
        argumentGrid.getColumnConstraints().add(colInfoName);
        argumentGrid.getColumnConstraints().add(colInfoValue);

        RowConstraints headerRowinfo = new RowConstraints();
        headerRowinfo.setFillHeight(true);
        argumentGrid.getRowConstraints().add(headerRowinfo);

        logPanel.setApiCallHistory(apiCallHistory);

        //if content of scrollpane is smaller than scrollpane, resize it to fit
////        argumentScrollPane.setFitToHeight(true);
//        argumentScrollPane.setFitToWidth(true);

////        argumentGrid.setPrefHeight(Region.USE_COMPUTED_SIZE);
////        argumentGrid.setPrefWidth(Region.USE_COMPUTED_SIZE);
//
//        argumentScrollPane.prefViewportWidthProperty().bind(argumentGrid.prefWidthProperty());
//        argumentScrollPane.prefViewportHeightProperty().bind(argumentGrid.prefHeightProperty());
//
////        argumentScrollPane.maxWidthProperty().bind(argumentGrid.prefWidthProperty());
////        argumentScrollPane.maxHeightProperty().bind(argumentGrid.prefHeightProperty());
////        argumentScrollPane.maxHeightProperty().set(Region.USE_PREF_SIZE);
//
////        argumentScrollPane.prefWidthProperty().bind(argumentGrid.prefWidthProperty());
////        argumentScrollPane.prefHeightProperty().bind(argumentGrid.prefHeightProperty());
    }

    private void selectMethod(MethodInfo oldMethod, MethodInfo newMethod) {
        if (oldMethod != null || newMethod != null) {
            for (CommandParameterController controller : argumentControllers) {
                controller.remove(argumentGrid);
            }
            argumentControllers.clear();
            argumentModels.clear();

            argumentGrid.getRowConstraints().clear();
        }

        if (commandHelp != null)
            commandNameBox.getChildren().remove(commandHelp);

        if (newMethod != null) {
            assert argumentModels.isEmpty();

            RowConstraints headerRowinfo = new RowConstraints();
            headerRowinfo.setFillHeight(true);
            argumentGrid.getRowConstraints().add(headerRowinfo);

            int rowIndex = 1;
            argumentModels.setAll(createCommandParameterModels(newMethod));
            for (CommandParameterModel model : argumentModels) {
                RowConstraints rowinfo = new RowConstraints();
                rowinfo.setFillHeight(true);
//                rowinfo.setPercentHeight(50);
                argumentGrid.getRowConstraints().add(rowinfo);

                CommandParameterController controller =
                        new CommandParameterController(commandArgumentChoosterFactory, model, argumentControllers);
                controller.add(argumentGrid, rowIndex++);

                argumentControllers.add(controller);
            }

            commandHelp = new ExpandableHelp(newMethod.annotation.hint());
            commandNameBox.getChildren().add(commandHelp);
            commandNameLabel.setText(newMethod.name);
            if (!commandNameLabel.getStyleClass().contains("boldText"))
                commandNameLabel.getStyleClass().add("boldText");

            userCustomServerRadioButton.setVisible(newMethod.api.requiresConnection); //custom server URL only works with connections, not with authority selection
        } else {
            commandNameLabel.setText("None selected");
            commandNameLabel.getStyleClass().remove("boldText");
        }

        updateFixedServerUrlField(currentlySelectedTreeApi());
    }

    @SuppressWarnings("unchecked")
    public List<CommandParameterModel> createCommandParameterModels(MethodInfo methodInfo) {
        List<CommandParameterModel> parameterModels = new ArrayList<>();

        Method method = methodInfo.method;

        Class<?>[] parameterTypes = method.getParameterTypes();
        Annotation[][] annotations = method.getParameterAnnotations();

        for (int i = 0; i < parameterTypes.length; i++) {
            Annotation[] paramAnnotations = annotations[i];
            Class<?> paramClass = parameterTypes[i];

            for (Annotation paramAnnotation : paramAnnotations)
                if (Objects.equals(paramAnnotation.annotationType(), ApiMethodParameter.class)) {
                    ApiMethodParameter amp = (ApiMethodParameter) paramAnnotation;

                    CommandParameterModel commandParameterModel =
                            new CommandParameterModel(amp.name(), paramClass, amp, methodInfo);

                    parameterModels.add(commandParameterModel);
                }
        }

        return parameterModels;
    }

    @FXML
    private void updateProxy() {
        JFedConnection.ProxyInfo proxyInfo = getProxyInfo();
        if (proxyInfo == null) {
            if (useAuthProxyRadioButton.isSelected())
                proxyFeedbackLabel.setText("No Authority Proxy can be used. " + lastProxyError);
            else
                proxyFeedbackLabel.setText("Proxy not configured correctly. " + lastProxyError);
        } else {
            proxyFeedbackLabel.setText("Proxy will be used.");
            if (useAuthProxyRadioButton.isSelected()) {
                //use info already derived
                JFedConnection.SshProxyInfo sshProxyInfo = (JFedConnection.SshProxyInfo) proxyInfo;
                proxyUsername.setText(sshProxyInfo.getUsername());
                proxyHostname.setText(sshProxyInfo.getHostname());
                proxyPort.setText(sshProxyInfo.getPort() + "");
            }
        }
    }

    private JFedConnection.ProxyInfo getProxyInfo() {
        if (!useSshProxyCheckBox.isSelected()) {
            lastProxyError = "proxy disabled";
            return null;
        }
        if (useAuthProxyRadioButton.isSelected())
            if (geniUserProvider.isUserLoggedIn()) {
                GeniUser geniUser = geniUserProvider.getLoggedInGeniUser();

                Server userAuth = geniUser.getUserAuthorityServer();
                if (!userAuth.getTestbed().getProxies().isEmpty()) { //currently, only SSH proxies are supported here!
                    Proxy pi = userAuth.getTestbed().getProxies().get(0);

                    assert Objects.equals(pi.getType(), "jFed SSH Proxy");

                    VlanRange range = new VlanRange(pi.getPortRange());
                    int port = range.getFirst();

                    LOG.info("SSH Proxy is activated, and there is an SSH Proxy for user authority " + userAuth.getName() + ": " + pi.getHostname() + ":" + port);

                    final GeniUser geniUser1 = geniUser;
                    return new JFedConnection.SshProxyInfo(
                            pi.getHostname(),
                            port,
                            geniUser.getUserUrn().getEncodedResourceName(),
                            SshKeyInfoFactory.createGeniUserSshKeyInfo(geniUser1),
                            pi.getHostKey(),
                            true/*limitName*/);
                } else {
                    lastProxyError = "user authority does not have SSH proxy";
                    return null;
                }
            } else {
                lastProxyError = "no user logged in";
                return null;
            }
        if (useCustomProxyRadioButton.isSelected()) {
            String username = proxyUsername.getText().trim();
            String hostname = proxyHostname.getText().trim();
            int port;
            try {
                port = Integer.parseInt(proxyPort.getText().trim());
            } catch (NumberFormatException e) {
                lastProxyError = "invalid port specified";
                return null;
            }
            if (username.isEmpty() || hostname.isEmpty()) {
                lastProxyError = "username and/or hostname empty";
                return null;
            }

            lastProxyError = "problem with private key";
            String privateKeyFilename = proxyPrivateKey.getText().trim();
            if (privateKeyFilename.isEmpty()) return null;
            String fileContent = null;
            try {
                fileContent = IOUtils.fileToString(privateKeyFilename);
            } catch (IOException e) {
                return null;
            }
            if (fileContent.isEmpty()) return null;
            boolean needPass = KeyUtil.hasEncryptedRsaPrivateKey(fileContent);
            PrivateKey privateKey = null;
            try {
                privateKey = KeyUtil.pemToAnyPrivateKey(fileContent, proxyPrivateKeyPass.getText().toCharArray());
            } catch (KeyUtil.PEMDecodingException e) {
                return null;
            }
            if (privateKey == null) return null;

            SshKeyInfo keyInfo;
            try {
                keyInfo = SshKeyInfoFactory.createBasicSshKeyInfoFromPrivateKeyOnly(privateKey);
            } catch (InvalidSshKeyInfoException e) {
                LOG.warn("Could not create a valid SshKeyInfo from the provided private key");
                return null;
            }

            return new JFedConnection.SshProxyInfo(
                    hostname,
                    port,
                    username,
                    keyInfo,
                    null/*hostkey*/,
                    false/*limitName*/);

        }
        lastProxyError =
                "unknown error: " + useAuthProxyRadioButton.isSelected() + " " + useCustomProxyRadioButton.isSelected();
        return null;
    }

    @FXML
    private void loadKeyFromFile() {
        FileChooser fileChooser = new FileChooser();
        File file = fileChooser.showOpenDialog(null);

        if (file != null) {
            proxyPrivateKey.setText(file.getPath());
        }

        updateProxy();
    }

    private boolean passwordsLoaded = false;

    @FXML
    private void call() {
        if (useSshProxyCheckBox.isSelected()) {
            JFedConnection.ProxyInfo proxyInfo = getProxyInfo();
            if (proxyInfo != null)
                connectionProvider.setDefaultProxy(proxyInfo);
            else
                connectionProvider.setDefaultProxy(null);
        } else
            connectionProvider.setDefaultProxy(null);


        boolean disableClientAuthentication = disableClientAuthenticationCheckBox.isSelected();

        MethodInfo calledMethod = currentlySelectedTreeMethod;
        if (calledMethod == null) return;
        NicknamedApiInfo calledApiInfo = calledMethod.api;
        assert calledApiInfo != null;
//        AbstractApi calledApi = calledApiInfo.api;

        if (LOG.isDebugEnabled()) {
            StringBuilder info = new StringBuilder();
            info.append("Call method ").append(calledMethod.name).append(" with parameters:");
            for (CommandParameterModel param : argumentModels) {
                if (param.isRequired() || param.includedProperty().get())
                    info.append("  - ").append(param.getParameterName()).append(" -> \"").append(param.valueProperty().get()).append("\"");
                else
                    info.append("  - Not included: ").append(param.getParameterName());
            }
            LOG.debug(info.toString());
        }

        try {
            JFedConnection con = null;
            Server auth = null;

            GeniUser geniUser = null;
            if (geniUserProvider.isUserLoggedIn())
                geniUser = geniUserProvider.getLoggedInGeniUser();

            if (!passwordsLoaded && geniUser != null) {
                jFedPasswordManager.load(jFedPreferences, geniUser);
                passwordsLoaded = true;
            }

            if (useUserRadioButton.isSelected()) {
                if (calledApiInfo.requiresConnection)
                    con =
                            connectionProvider.getConnectionByUserAuthority(geniUser, calledApiInfo.requiredServerType, disableClientAuthentication);
                if (calledApiInfo.requiresAuth)
                    auth = geniUser.getUserAuthorityServer();
            }
            if (userAuthorityRadioButton.isSelected()) {
                assert con == null;
                if (calledApiInfo.requiresConnection)
                    con = connectionProvider.getConnectionByAuthority(
                            geniUser,
                            authChooserController.getSelectedServer(),
                            calledApiInfo.requiredServerType,
//                            calledApiInfo.getAnyApi().getClass(), //this works only if it's an AbstactApi, but that shouldn't be a problem as requiresConnection will normally only be used in that case   (or if a non AbstractApi gets added to the getConnectionByAuthority)
                            disableClientAuthentication);
                if (calledApiInfo.requiresAuth)
                    auth = authChooserController.getSelectedServer();
            }
            if (userCustomServerRadioButton.isSelected()) {
                assert con == null;
                HandleUntrustedCallback handleUntrustedCallback;
                if (ignoreSelfSignedCheckBox.isSelected()) {
                    LOG.warn("SECURITY WARNING: making connection which accepts all certificates");
                    handleUntrustedCallback =
                            new BasicConnectionBuilder.INSECURE_TRUSTALL_HandleUntrustedCallback();
                } else {
                    handleUntrustedCallback = sslCertificateJFedInfo -> {
                        if (sslCertificateJFedInfo.isTrusted()) return true;

                        String problemDescription = "";
                        if (sslCertificateJFedInfo.isSelfSigned()) {
                            problemDescription += "The server's certificate is self signed. Certificate info:\n";
                            for (X509Certificate cert : sslCertificateJFedInfo.getChain()) {
                                problemDescription += "" + cert.toString();
                                problemDescription += "\n\n";
                            }
                        }
                        if (!sslCertificateJFedInfo.getSubjectMatchesHostname()) {
                            if (!Objects.equals(problemDescription, ""))
                                problemDescription += "\n\nADDITIONAL SECURITY PROBLEM:\n";
                            problemDescription +=
                                    "The certificate's subject hostname does not match the server URL:\n";
                            problemDescription +=
                                    "    Certificate Subject: " + sslCertificateJFedInfo.getSubject() + "\n";
                            problemDescription +=
                                    "    Server Hostname: " + sslCertificateJFedInfo.getHostname() + "\n";
                        }
                        ButtonType trustButtonType =
                                new ButtonType("I know what I am doing, I checked the certificate manually, and I trust the server", ButtonBar.ButtonData.YES);
                        ButtonType noTrustButtonType = new ButtonType("I do not trust this", ButtonBar.ButtonData.NO);

                        Optional<ButtonType> response = JFDialogs.create()
                                .owner(callButton)
                                .message(problemDescription)
                                .allowCopyMessage()
                                .buttonTypes(trustButtonType, noTrustButtonType).showConfirm();

                        return response.isPresent() && response.get() == trustButtonType;
                    };
                }
                if (calledApiInfo.requiresConnection) {
                    //TODO: provide a fake authority. JavaFXLogger.fireResult needs it.   (or alternatively, make it not need it)
//                    Map<ServerType, URL> urls = new HashMap<>();
//                    urls.put(calledApiInfo.requiredServerType, new URL(customServerUrlField.getText()));
//                    Server authForDebugInfo = new Server("urn:publicid:IDN+call_by_url+authority+cm", "Fake Authority for call by URL", null /*location*/,
//                            urls, Collections.emptyList(), null/*gid*/, "call by url"/*type*/);

                    con =
                            connectionProvider.getConnectionByUrl(null/*overrideTrustStore*/, geniUser, new URL(customServerUrlField.getText()),
                                    handleUntrustedCallback, calledApiInfo.requiredServerType, disableClientAuthentication, null/*authForDebugInfo*/,
                                    ignoreSelfSignedCheckBox.isSelected() ? Collections.singletonList("*") : null/*overrideAllowedServerCertificateAlias*/);
                    assert con != null : "Custom server URL connection was not setup";
                }
                if (calledApiInfo.requiresAuth) {
                    if (calledApiInfo.requiredServerType == null)
                        throw new RuntimeException("This is not supported using an URL");

                    throw new RuntimeException("This is not yet implemented");
//                    //make fake auth
//                    Server fakeAuth = new Server();
                    //TODO
//                    auth = fakeAuth;
                }
            }

            if (calledApiInfo.requiresConnection && con == null) {
                throw new RuntimeException("Authority to use for needed ServerType is unknown, or URL for needed ServerType is unknown: api=" + calledApiInfo.requiredServerType);
            }
            if (calledApiInfo.requiresAuth && auth == null) {
                throw new RuntimeException("Authority to use is unknown");
            }

            execute(calledMethod, con, auth, geniUser);
        } catch (Exception e) {
            throw new RuntimeException("Call failed: " + e.getMessage(), e);
        }
    }

    public void execute(MethodInfo methodInfo, final JFedConnection connection, final Server auth, final GeniUser geniUser) throws JFedException {
        final Method method = methodInfo.method;
        final NicknamedApiInfo nicknamedApiInfo = methodInfo.api;
        final Object targetObject = nicknamedApiInfo.getAnyApi(auth);

        int chooserIndex = 0;

        final Class[] parameterTypes = method.getParameterTypes();
        Annotation[][] annotations = method.getParameterAnnotations();

        final Object[] parameters = new Object[parameterTypes.length];

        for (int i = 0; i < parameterTypes.length; i++) {
            Annotation[] paramAnnotations = annotations[i];
            Class paramClass = parameterTypes[i];

            if (JFedConnection.class.isAssignableFrom(paramClass)) {
                if (!nicknamedApiInfo.requiresConnection)
                    throw new RuntimeException("This is configured as NOT requiring a connection, yet a JFedConnection class was found as parameter! -> BUG!");

                parameters[i] = connection;
            } else {
                if (Logger.class.isAssignableFrom(paramClass)) {
                    parameters[i] = logger;
                } else {
                    if (Objects.equals(paramClass, GeniUser.class)) { //TODO is this still useful? What is this for?
                        assert geniUser != null : "parameter required user, but no user is provided";
                        parameters[i] = geniUser;
                    } else {
                        for (Annotation paramAnnotation : paramAnnotations) {
                            if (Objects.equals(paramAnnotation.annotationType(), ApiMethodParameter.class)) {
                                ApiMethodParameter amp = (ApiMethodParameter) paramAnnotation;
                                CommandParameterModel commandParameterModel = argumentModels.get(chooserIndex++);
                                Object c = null;
                                if (commandParameterModel.isRequired() || commandParameterModel.includedProperty().get()) {
                                    c = commandParameterModel.valueProperty().get();
                                    assert c != null :
                                            "c==null when isRequired=" + commandParameterModel.isRequired() + " included=" + commandParameterModel.includedProperty().get() +
                                                    " for " + commandParameterModel.getParameterName();

                                    //The GUI elements sometimes return "higher level" objects, that the low level API does not know.
                                    //We extract the lower level Objects that the API understands from them here
                                    //Also, some classes can be converted to and from string, so here we make sure the correct one is used

                                    //ResourceId needs to be converted to String in some cases
                                    if (ResourceId.class.isInstance(c) && Objects.equals(paramClass, String.class))
                                        c = ((ResourceId) c).getValue();
                                    //String needs to be converted to other classes in some cases
                                    if (String.class.isInstance(c) && Objects.equals(paramClass, ResourceId.class))
                                        c = ResourceIdParser.parse((String) c);
                                    if (String.class.isInstance(c) && Objects.equals(paramClass, ResourceUrn.class))
                                        c = new ResourceUrn((String) c);
                                    if (String.class.isInstance(c) && Objects.equals(paramClass, Date.class))
                                        try {
                                            c = RFC3339Util.rfc3339StringToDate((String) c);
                                        } catch (ParseException e) {
                                            throw new RuntimeException("Failed to convert user specified String to Date. Make sure the date is in RFC3339 format.", e);
                                        }
                                    if (String.class.isInstance(c) && Objects.equals(paramClass, GeniUrn.class))
                                        try {
                                            c = new GeniUrn((String) c);
                                        } catch (GeniUrn.GeniUrnParseException e) {
                                            throw new RuntimeException("Failed to convert user specified String to GeniUrn. Is it a valid urn?", e);
                                        }

                                    //list of CredentialInfo needs to be converted to list of AnyCredential
                                    if (amp.name().equals("credentialList") || amp.parameterType().equals(ApiMethodParameterType.LIST_OF_CREDENTIAL)) {
                                        assert c != null;
                                        assert List.class.isInstance(c) :
                                                "Argument with name \"credentialList\" has type " + c.getClass().getName() + " instead of List. Value: " + c;

                                        List<AnyCredential> credentials = new ArrayList<>();
                                        List l = (List) c;

                                        for (Object o : l) {

                                            if (AnyCredential.class.isInstance(o)) {
                                                AnyCredential cred = (AnyCredential) o;
                                                credentials.add(cred);
                                            } else {
                                                throw new RuntimeException("In list of credentials, expected AnyCredential but got incompatible " + o.getClass() + ".");
                                            }

                                        }

                                        c = credentials;
                                    }
                                }

                                if (c != null && !paramClass.isInstance(c))
                                    LOG.warn("WARNING: (for argument " + amp.name() + ") expected class " + paramClass + " but got incompatible " + c.getClass() + ". This will cause an error later on.");

                                if (c == null)
                                    LOG.debug("Using null for parameter nr " + i + " of " + paramClass.getName() + " with name " + amp.name());

                                parameters[i] = c;
                            }
                        }
                    }
                }
            }

            if (parameters[i] == null) {
                LOG.debug("Ignoring parameter nr "+i+" of "+paramClass.getName()+"  (probably because of no annotation)");
            }

            LOG.trace("DEBUG CommandPanel.execute parameters[" + i + "]=" + parameters[i]);
        }

        Task call = new Task("Probe " + methodInfo.name + " Call") {
            @Override
            public void doTask(TaskExecution singleTask) throws JFedException, InterruptedException {
                try {
                    //Note: add and remove listener is NOT the same as adding the logger. The difference is that an added
                    // logger will see calls to other API's on other threads as well, since it is added to the global
                    // logger instead of a logger specific for this callLogger myLogger


                    //wvdemeer: not sure if this logging business still does anything useful
                    Logger myLogger = null;
                    Logger globalLogger = null;
                    if (targetObject instanceof AbstractApi) {
                        AbstractApi abstractApi = (AbstractApi) targetObject;

                        globalLogger = abstractApi.getLogger();
                        assert globalLogger instanceof JavaFXLogger;
                        myLogger = globalLogger.getWrappingLogger(singleTask);
                        assert myLogger instanceof JavaFXLogger;

                        abstractApi.setLogger(myLogger);

//               don't do this:     abstractApi.getLogger().addResultListener(singleTask);
                    }

                    try {
                        Object methodRes = method.invoke(targetObject, parameters);
                    } catch (Exception e) { //IllegalAccessException IllegalArgumentException InvocationTargetException
                        LOG.error("Error invoking method {} with parameters {}", methodInfo.name, parameters, e);
                        throw e;
                    }

                    if (targetObject instanceof AbstractApi) {
                        assert myLogger != null;
                        assert globalLogger != null;

                        AbstractApi abstractApi = (AbstractApi) targetObject;
                        abstractApi.setLogger(globalLogger);

//               don't do this:     abstractApi.getLogger().removeResultListener(singleTask);

                        //cleanup: remove singleTask from myLogger
                        myLogger.removeResultListener(singleTask);
                    }

                    LOG.info("Calling probe task completed");

                    //TODO: do something with result if not ApiCallReply?   (ApiCallReply is shown in log, result might not be)
                    //ApiCallReply methodResRep = (ApiCallReply) methodRes;
                    //invoke hides InterruptedException
//                } catch (InterruptedException e) {
//                    throw e;
//                }
                } catch (Exception e) { //TODO better exception handling?
                    if (e instanceof InterruptedException)
                        throw (InterruptedException) e;

                    LOG.error("Exception calling probe task", e);
                    e.printStackTrace();
                    if (e.getCause() != null && e.getCause() instanceof JFedException) {
                        //                System.err.println("Note: GeniException in invoked call => unwrapping GeniException and throwing it. see CommandPanel.execute(...)");
                        throw (JFedException) e.getCause();
                    }
                    throw new JFedException("Exception invoking API call", e);
                }
            }
        };
        tt.addTask(call);
    }

    public static class NicknamedApiInfo {
        public final String nickname;
        public final AbstractApi abstractApi;
        public final Object otherApi;
        public final ApiInfo.Api requiredServerType;
        public final boolean requiresConnection;
        public final boolean requiresUserAuthConnection;
        public final boolean requiresAuth;
        public final boolean requiresUserAuth;

        private NicknamedApiInfo(String nickname, Object api,
                                 ApiInfo.Api requiredServerType,
                                 boolean requiresConnection,
                                 boolean requiresUserAuthConnection,
                                 boolean requiresAuth,
                                 boolean requiresUserAuth) {
            this.nickname = nickname;
            if (api != null) {
                if (api instanceof AbstractApi) {
                    this.abstractApi = (AbstractApi) api;
                    this.otherApi = null;
                } else {
                    this.otherApi = api;
                    this.abstractApi = null;
                }
            } else {
                //this is only possible if the method getAnyApi(auth) get overridden
                this.otherApi = null;
                this.abstractApi = null;
            }
            this.requiredServerType = requiredServerType;
            this.requiresConnection = requiresConnection;
            this.requiresUserAuthConnection = requiresUserAuthConnection;
            this.requiresAuth = requiresAuth;
            this.requiresUserAuth = requiresUserAuth;

            //checks if all filled in correctly
            assert requiresConnection == (requiredServerType != null);
            if (requiresUserAuthConnection)
                assert requiresConnection;
            if (requiresUserAuth)
                assert requiresAuth;
        }

        private NicknamedApiInfo(AbstractApi api) {
            this(api.getName(), api, guessRequiresUserAuthConnection(api));
        }

        private NicknamedApiInfo(AbstractApi api, boolean requiresUserAuthConnection) {
            this(api.getName(), api, requiresUserAuthConnection);
        }

        private NicknamedApiInfo(String nickname, AbstractApi api) {
            this(nickname, api, guessRequiresUserAuthConnection(api));
        }

        private NicknamedApiInfo(String nickname, AbstractApi api, boolean requiresUserAuthConnection) {
            this(nickname, api, api.getApi(), true, requiresUserAuthConnection, false, false);
        }

        private static boolean guessRequiresUserAuthConnection(AbstractApi api) {
            return api.getApi().getName().getAuthentication() == ConnectionConfig.Authentication.SSL_CLIENT_AUTH;
        }

        public Object getAnyApi(Server auth) {
            assert abstractApi == null ^ otherApi == null;
            if (abstractApi != null)
                return abstractApi;
            return otherApi;
        }

        public AbstractApi getAbstractApi() {
            assert abstractApi != null;
            assert otherApi == null;
            return abstractApi;
        }
    }

    private static class CommandParameterController {

        private final CommandArgumentChooserFactory commandArgumentChooserFactory;
        public final CommandParameterModel model;

        public final CheckBox includedCheckBox;
        public final Label requiredLabel;
        public final Control includedColumnItem;
        public final VBox includedColumnItemBox;

        public final Label nameLabel;
        public final VBox nameLabelBox;
        public final VBox commandArgumentChooserBox;
        public final ExpandableHelp commandArgumentChooserHelper;
        public final CommandArgumentChooser commandArgumentChooser;

        private final ObservableList<CommandParameterController> otherControllers;
        private final ObservableList<CommandArgumentChooser> otherCommandArgumentChoosers;


        public CommandParameterController(CommandArgumentChooserFactory commandArgumentChooserFactory,
                                          CommandParameterModel model,
                                          ObservableList<CommandParameterController> otherControllers) {
            this.commandArgumentChooserFactory = commandArgumentChooserFactory;
            this.model = model;
            this.otherControllers = otherControllers;
            this.otherCommandArgumentChoosers = new OtherCommandArgumentsObservableList(otherControllers);

            includedCheckBox = new CheckBox();
            requiredLabel = new Label("required");

            if (model.isRequired()) {
                includedColumnItem = requiredLabel;
            } else {
                includedColumnItem = includedCheckBox;
                includedCheckBox.selectedProperty().bindBidirectional(model.includedProperty());
            }
            includedColumnItemBox = new VBox();
            includedColumnItemBox.setPadding(new Insets(10));
            includedColumnItemBox.getChildren().add(includedColumnItem);

            nameLabel = new Label(model.getParameterName());
            nameLabelBox = new VBox();
            nameLabelBox.setPadding(new Insets(10));
            nameLabelBox.getChildren().add(nameLabel);

            commandArgumentChooser = commandArgumentChooserFactory.getCommandArgumentChooser(
                    model.getParameterName(),
                    model.getParameterClass(),
                    model.getAnnotation(),
                    model.getMethodInfo(),
                    otherCommandArgumentChoosers);
            assert commandArgumentChooser != null;
            assert commandArgumentChooser.valueProperty() != null :
                    "commandArgumentChooser.valueProperty() is null for name=" + model.getParameterName() + " class=" + model.getParameterClass() + " chooserClass=" + commandArgumentChooser.getClass().getName();
            model.valueProperty().bind(commandArgumentChooser.valueProperty());
            commandArgumentChooser.disableProperty().bind(model.includedProperty().not()); //disable if not included

            commandArgumentChooserBox = new VBox();
            commandArgumentChooserBox.setPadding(new Insets(10));
            String helpText = model.getAnnotation().hint();
            commandArgumentChooserHelper = new ExpandableHelp(helpText);
            if (helpText != null && !helpText.isEmpty())
                commandArgumentChooserBox.getChildren().add(commandArgumentChooserHelper);
            commandArgumentChooserBox.getChildren().add(commandArgumentChooser);

            commandArgumentChooserHelper.setMaxWidth(500.0);
            commandArgumentChooser.setMaxWidth(500.0);
        }

        /*
         * add this item to the argument grid
         * */
        public void add(GridPane gridPane, int row) {
            GridPane.setConstraints(includedColumnItemBox, 0/*col*/, row/*row*/);
            GridPane.setConstraints(nameLabelBox, 1, row);
            GridPane.setConstraints(commandArgumentChooserBox, 2, row);

//            GridPane.setMargin(includedColumnItem, new Insets(10, 10, 10, 10));
            GridPane.setHalignment(includedColumnItemBox, HPos.CENTER);

//            GridPane.setMargin(nameParent, new Insets(10, 10, 10, 10));
            GridPane.setHalignment(nameLabelBox, HPos.RIGHT);

//            GridPane.setMargin(commandArgumentChooserBox, new Insets(10, 10, 10, 10));
            GridPane.setHalignment(commandArgumentChooserBox, HPos.CENTER);

            GridPane.setHgrow(includedColumnItemBox, Priority.NEVER);
            GridPane.setHgrow(nameLabelBox, Priority.NEVER);
            GridPane.setHgrow(commandArgumentChooserBox, Priority.ALWAYS);

            includedColumnItem.getStyleClass().removeAll("argumentGridA", "argumentGridB");
            includedColumnItemBox.getStyleClass().removeAll("argumentGridA", "argumentGridB");
            nameLabel.getStyleClass().removeAll("argumentGridA", "argumentGridB");
            nameLabelBox.getStyleClass().removeAll("argumentGridA", "argumentGridB");
            commandArgumentChooserBox.getStyleClass().removeAll("argumentGridA", "argumentGridB");
//            if (row % 2 == 0) {
//                includedColumnItem.getStyleClass().add("argumentGridA");
//                nameLabel.getStyleClass().add("argumentGridB");
//                commandArgumentChooserBox.getStyleClass().add("argumentGridA");
//            } else {
//                includedColumnItem.getStyleClass().add("argumentGridB");
//                nameLabel.getStyleClass().add("argumentGridA");
//                commandArgumentChooserBox.getStyleClass().add("argumentGridB");
//            }
            if (row % 2 == 0) {
                includedColumnItem.getStyleClass().add("argumentGridA");
                includedColumnItemBox.getStyleClass().add("argumentGridA");
                nameLabel.getStyleClass().add("argumentGridA");
                nameLabelBox.getStyleClass().add("argumentGridA");
                commandArgumentChooserBox.getStyleClass().add("argumentGridA");
            } else {
                includedColumnItem.getStyleClass().add("argumentGridB");
                includedColumnItemBox.getStyleClass().add("argumentGridB");
                nameLabel.getStyleClass().add("argumentGridB");
                nameLabelBox.getStyleClass().add("argumentGridB");
                commandArgumentChooserBox.getStyleClass().add("argumentGridB");
            }

            gridPane.getChildren().add(includedColumnItemBox);
            gridPane.getChildren().add(nameLabelBox);
            gridPane.getChildren().add(commandArgumentChooserBox);

            includedColumnItem.setMinWidth(includedColumnItem.getMaxWidth());
            nameLabel.setMinWidth(nameLabel.getMaxWidth());
            includedColumnItem.setPrefWidth(includedColumnItem.getMaxWidth());
            nameLabel.setPrefWidth(nameLabel.getMaxWidth());

            includedColumnItem.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
            nameLabel.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
            commandArgumentChooserBox.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);

            gridPane.getStyleClass().add("argumentGrid");
        }

        public void remove(GridPane gridPane) {
            model.valueProperty().unbind();
            boolean removedItem = gridPane.getChildren().remove(includedColumnItemBox);
            boolean removedItem2 = gridPane.getChildren().remove(nameLabelBox);
            boolean removedItem3 = gridPane.getChildren().remove(commandArgumentChooserBox);
            assert removedItem;
            assert removedItem2;
            assert removedItem3;
        }

        public static class OtherCommandArgumentsObservableList extends ListBinding<CommandArgumentChooser> {
            private final ObservableList<CommandParameterController> otherControllers;

            public OtherCommandArgumentsObservableList(ObservableList<CommandParameterController> otherControllers) {
                this.otherControllers = otherControllers;
                otherControllers.addListener((ListChangeListener<CommandParameterController>) change -> {

                });
            }

            private List<CommandArgumentChooser> buildList() {
                List<CommandArgumentChooser> res = new ArrayList<>();
                if (otherControllers == null) return res;
                res.addAll(otherControllers.stream().filter(c -> c.commandArgumentChooser != null).map(c -> c.commandArgumentChooser).collect(Collectors.toList()));
                return res;
            }

            /**
             * using computeValue() here is not very efficient, but it is good enough. (it's just for a limited number of GUI components)
             */
            @Override
            protected ObservableList<CommandArgumentChooser> computeValue() {
                return FXCollections.observableArrayList(buildList());
            }
        }
    }

    public static class MethodInfo {
        public final NicknamedApiInfo api;
        public final Method method;
        public final ApiMethod annotation;
        public final String name;

        public MethodInfo(NicknamedApiInfo api, Method method, ApiMethod annotation) {
            this.api = api;
            this.method = method;
            this.annotation = annotation;
            this.name = method.getName();
        }

        public MethodInfo(NicknamedApiInfo api, Method method) {
            this(api, method, method.getAnnotation(ApiMethod.class));
        }

        public String toString() {
            return name;
        }
    }
}
