package be.iminds.ilabt.jfed.experimenter_gui.dialogs;

import be.iminds.ilabt.jfed.experimenter_gui.config.JFedGuiConfig;
import be.iminds.ilabt.jfed.experimenter_gui.ext_requests.JFedEnvironment;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.model.SfaModel;
import be.iminds.ilabt.jfed.highlevel.model.Slice;
import be.iminds.ilabt.jfed.highlevel.tasks.HighLevelTaskFactory;
import be.iminds.ilabt.jfed.highlevel.tasks.ListSubAuthoritiesTask;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.UserAndSliceApiWrapper;
import be.iminds.ilabt.jfed.preferences.JFedGuiPreferences;
import be.iminds.ilabt.jfed.ui.javafx.dialogs.JFDialogs;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableMap;
import javafx.collections.WeakListChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.util.StringConverter;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.validation.ValidationMessage;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.GraphicValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * User: twalcari
 * Date: 2/24/14
 * Time: 4:03 PM
 */
public class StartExperimentController extends GridPane {
    private static final Logger LOG = LoggerFactory.getLogger(StartExperimentController.class);

    private static final Duration MINIMUM_SLICE_DURATION = Duration.ofMinutes(1);
    private static final int MAX_DURATION_DAYS = 100;
    private final HighLevelTaskFactory hltf;
    private final GeniUserProvider geniUserProvider;
    private final SfaModel sfaModel;
    private final JFedEnvironment environment;
    private final JFedGuiConfig config;
    private final JFedGuiPreferences preferences;


    private final ObservableMap<String, Slice> slices = FXCollections.observableHashMap();
    private final ListChangeListener<Slice> sliceListChangeListener = c -> {
        while (c.next()) {
            c.getAddedSubList().forEach(slice -> slices.put(slice.getName(), slice));
            c.getRemoved().forEach(slice -> slices.remove(slice.getName()));
        }
    };

    private int maximumSliceNameLength = 8;


    @FXML
    protected TextField nameTextField;
    @FXML
    protected Button showExistingSlicesButton;
    @FXML
    protected ComboBox<Integer> hoursComboBox;
    @FXML
    protected ComboBox<Integer> minutesComboBox;
    @FXML
    protected ComboBox<Integer> daysComboBox;
    @FXML
    protected Label subAuthLabel;
    @FXML
    protected CheckBox subAuthCheckBox;
    @FXML
    protected ProgressBar subAuthFetchProgressBar;
    @FXML
    protected ComboBox<String> subAuthComboBox;
    @FXML
    protected Button refreshSubAuthButton;
    @FXML
    protected CheckBox includeSshKeys;
    @FXML
    protected CheckBox requestSlaCheckBox;
    private int userSetDays = 0, userSetHours = 2, userSetMinutes = 0;

    private final InvalidationListener expirationDateTimeInvalidationListener = new InvalidationListener() {
        @Override
        public void invalidated(Observable observable) {
            userSetDays = daysComboBox.getValue();
            userSetHours = hoursComboBox.getValue();
            userSetMinutes = minutesComboBox.getValue();
        }
    };
    private final ValidationSupport validationSupport;
    private final boolean canRequestProjectMemberSshKeys;
    private boolean prevRequestProjectMemberSshKeysValue;

    @Inject
    StartExperimentController(HighLevelTaskFactory hltf, GeniUserProvider geniUserProvider,
                              SfaModel sfaModel, JFedEnvironment environment, JFedGuiConfig config, JFedGuiPreferences preferences) {
        this.hltf = hltf;
        this.geniUserProvider = geniUserProvider;
        this.sfaModel = sfaModel;
        this.environment = environment;
        this.config = config;
        this.preferences = preferences;

        validationSupport = new ValidationSupport();
        validationSupport.setValidationDecorator(new GraphicValidationDecoration());

        Server userServer =
                this.geniUserProvider.getLoggedInGeniUser().getUserAuthorityServer();
        assert userServer != null;
        canRequestProjectMemberSshKeys = userServer.hasFlag(Server.Flag.featureFedMaRequestMemberSshKey);
    }

    @FXML
    public void initialize() {
        //populate date/time combo-boxes

        //check if there is a startup parameter that defines the default duration, otherwise use the values set in userSet{days, hours, minutes}
        if (environment.getDefaultSliceDuration() != null) {
            int defaultSliceDuration = environment.getDefaultSliceDuration();

            userSetMinutes = defaultSliceDuration % 60;
            defaultSliceDuration /= 60;
            userSetHours = defaultSliceDuration % 24;
            userSetDays = Math.min(defaultSliceDuration / 24, MAX_DURATION_DAYS);
        }


        daysComboBox.getItems().addAll(IntStream.range(0, MAX_DURATION_DAYS).boxed().collect(Collectors.toList()));
        daysComboBox.getSelectionModel().select(userSetDays);

        hoursComboBox.getItems().addAll(IntStream.range(0, 24).boxed().collect(Collectors.toList()));
        hoursComboBox.getSelectionModel().select(userSetHours);

        minutesComboBox.getItems().addAll(IntStream.range(0, 60).boxed().collect(Collectors.toList()));
        minutesComboBox.setConverter(new StringConverter<Integer>() {
            @Override
            public String toString(Integer integer) {
                return String.format("%02d", integer);
            }

            @Override
            public Integer fromString(String s) {
                return Integer.parseInt(s);
            }
        });
        minutesComboBox.getSelectionModel().select(userSetMinutes);

        //make our slices-variable reflect the currently known slices
        sfaModel.getSlices().forEach(slice -> slices.put(slice.getName(), slice));
        sfaModel.getSlices().addListener(new WeakListChangeListener<>(sliceListChangeListener));

        TextFields.bindAutoCompletion(nameTextField, slices.keySet());

        subAuthCheckBox.selectedProperty().addListener(observable -> {
            if (subAuthCheckBox.isSelected())
                enableSubAuthoritiesList();
            else
                disableSubAuthoritiesList();
        });
        subAuthCheckBox.selectedProperty().addListener(observable -> {
            if (!subAuthCheckBox.isSelected()) {
                prevRequestProjectMemberSshKeysValue = includeSshKeys.isSelected();
                includeSshKeys.setSelected(false);

                includeSshKeys.setDisable(true);
            } else {
                includeSshKeys.setSelected(prevRequestProjectMemberSshKeysValue);
                includeSshKeys.setDisable(false);
            }
        });

        enableSliceDetailsEditing();

        final InvalidationListener sliceNameInvalidationListener = observable -> {
            String sliceName = nameTextField.getText();

            if (slices.containsKey(sliceName)) {
                showFixedSliceDetails(slices.get(sliceName));
            } else {
                enableSliceDetailsEditing();
            }
        };

        nameTextField.textProperty().addListener(sliceNameInvalidationListener);

        Platform.runLater(nameTextField::requestFocus);


        includeSshKeys.managedProperty().bind(includeSshKeys.visibleProperty());

        includeSshKeys.setVisible(canRequestProjectMemberSshKeys);
        includeSshKeys.setSelected(canRequestProjectMemberSshKeys && preferences.getAddProjectMembersSshKeysByDefault());

        requestSlaCheckBox.setVisible(config.isSlaAvailable());

        validationSupport.registerValidator(nameTextField, false, sliceNameValidator);
        validationSupport.<String>registerValidator(subAuthComboBox,
                Validator.createPredicateValidator(
                        s -> !subAuthCheckBox.isSelected() || !subAuthComboBox.getSelectionModel().isEmpty(),
                        "Please choose a project in which this experiment should be run!"));
    }

    private boolean sliceExpirationEditingEnabled = false;

    //is set to false, so that enableSliceDetailsEditing() would successfully run the first time it is called
    private boolean sliceDetailsEditingEnabled = false;

    private void enableSliceDetailsEditing() {
        if (sliceDetailsEditingEnabled)
            return;

        initializeSubAuthorities();

        sliceDetailsEditingEnabled = true;
    }

    private void disableSliceDetailsEditing() {
        if (!sliceDetailsEditingEnabled)
            return;

        subAuthCheckBox.setDisable(true);
        subAuthComboBox.setDisable(true);

        sliceDetailsEditingEnabled = false;
    }

    private void enableSliceExpirationEditing() {
        if (sliceExpirationEditingEnabled)
            return;

        daysComboBox.setDisable(false);
        hoursComboBox.setDisable(false);
        minutesComboBox.setDisable(false);

        daysComboBox.setValue(userSetDays);
        hoursComboBox.setValue(userSetHours);
        minutesComboBox.setValue(userSetMinutes);

        daysComboBox.valueProperty().addListener(expirationDateTimeInvalidationListener);
        hoursComboBox.valueProperty().addListener(expirationDateTimeInvalidationListener);
        minutesComboBox.valueProperty().addListener(expirationDateTimeInvalidationListener);

        sliceExpirationEditingEnabled = true;
    }

    private void disableSliceExpirationEditing() {
        if (!sliceExpirationEditingEnabled)
            return;

        daysComboBox.valueProperty().removeListener(expirationDateTimeInvalidationListener);
        hoursComboBox.valueProperty().removeListener(expirationDateTimeInvalidationListener);
        minutesComboBox.valueProperty().removeListener(expirationDateTimeInvalidationListener);

        daysComboBox.setDisable(true);
        hoursComboBox.setDisable(true);
        minutesComboBox.setDisable(true);

        sliceExpirationEditingEnabled = false;
    }

    private void showFixedSliceDetails(Slice slice) {
        if (sliceDetailsEditingEnabled) {
            disableSliceDetailsEditing();
        }

        enableSliceExpirationEditing();

        if (slice.getSubAuthorityName() != null) {
            subAuthCheckBox.setSelected(true);
            subAuthComboBox.setValue(slice.getSubAuthorityName());
        } else {
            subAuthCheckBox.setSelected(false);
            subAuthComboBox.setValue(null);
        }

        //fix the expiration-time to that of the slice
        if (slice.getExpirationDate() != null) {
            Duration duration = Duration.between(Instant.now(), slice.getExpirationDate());

            long time = duration.toMinutes();

            minutesComboBox.setValue((int) (time % 60));
            time /= 60;

            hoursComboBox.setValue((int) (time % 24));
            time /= 24;

            daysComboBox.setValue((int) time);
        }// else {
        //do nothing, value will already be initialised

//            daysComboBox.getSelectionModel().clearSelection();
//            hoursComboBox.getSelectionModel().clearSelection();
//            minutesComboBox.getSelectionModel().clearSelection();
        //  }
    }

    private final Validator<String> sliceNameValidator = (control, sliceName) -> {
        ValidationResult result = new ValidationResult();
        if (sliceName == null) {
            result.add(ValidationMessage.error(control, "Experiment name cannot be empty!"));
            return result;
        }

        if (slices.containsKey(sliceName)) {
            //always allow existing slices!
            return result;
        }

        if (sliceName.length() == 0) {
            result.add(ValidationMessage.error(control, "Experiment name cannot be empty!"));
        } else {
            if (sliceName.length() > maximumSliceNameLength) {
                if (maximumSliceNameLength > 5)
                    result.add(ValidationMessage.error(control,
                            String.format("Experiment name cannot be longer than %d characters", maximumSliceNameLength)));
                else {
                    if (maximumSliceNameLength <= 0)
                        result.add(ValidationMessage.error(control,
                                String.format("Your nodes names are too long!" +
                                        "Try to remove at least %d chars from the longest node name(s)", (3 - maximumSliceNameLength))));
                    else
                        result.add(ValidationMessage.error(control,
                                String.format("Experiment name cannot be longer than %d characters. You can try to shorten node names to increase this limit.", maximumSliceNameLength)));
                }
            }
        }

        for (char c : sliceName.toCharArray()) {
            boolean validChar = (c >= 'a' && c <= 'z')
                    || (c >= 'A' && c <= 'Z')
                    || (c >= '0' && c <= '9');

            if (!validChar) {
                result.add(ValidationMessage.error(control,
                        String.format("Experiment name may not contain the character '%c'." +
                                " Only characters [a-zA-Z0-9] are allowed", c)));
            }
        }

        return result;
    };

    private void initializeSubAuthorities() {
        //initialize subauthorities-list
        if (sfaModel.getSubAuthoritySupport()
                == UserAndSliceApiWrapper.SubAuthoritySupport.SUB_AUTHORITY_FORBIDDEN) {
            subAuthCheckBox.setSelected(false);
            subAuthCheckBox.setDisable(true);

            subAuthFetchProgressBar.setVisible(false);
            subAuthComboBox.setDisable(true);
            subAuthComboBox.setItems(FXCollections.observableArrayList("Not supported"));
            refreshSubAuthButton.setDisable(true);
        } else {

            subAuthCheckBox.setSelected(true);
            if (sfaModel.getSubAuthoritySupport()
                    == UserAndSliceApiWrapper.SubAuthoritySupport.SUB_AUTHORITY_MANDATORY) {
                subAuthCheckBox.setDisable(true);
            } else {
                subAuthCheckBox.setDisable(false);
                //default: use a sub authority (because it is generally nicer)
            }
            enableSubAuthoritiesList();
        }
    }

    private void showSubAuthoritiesList() {
        //this method should not be executed when SubAuthority-checkbox is not selected
        if (!subAuthCheckBox.isSelected()) {
            LOG.warn("unexpected: showSubAuthoritiesList() called while subAuthCheckBox.isSelected()=" + !subAuthCheckBox.isSelected());
            return;
        }

        List<String> subAuthNames = new ArrayList<>(sfaModel.getLoggedInUserSubAuthorityNames());
        Collections.sort(subAuthNames);

        subAuthComboBox.setItems(FXCollections.observableList(subAuthNames));
//        Collections.sort(subAuthComboBox.getItems());

        subAuthComboBox.setDisable(false);

        if (!subAuthComboBox.getItems().isEmpty())
            subAuthComboBox.getSelectionModel().selectFirst();

        if (subAuthComboBox.getItems().isEmpty() && sfaModel.getSubAuthoritySupport() == UserAndSliceApiWrapper.SubAuthoritySupport.SUB_AUTHORITY_MANDATORY)
            JFDialogs.create()
                    .owner(subAuthComboBox)
                    .message("Selecting a project is mandatory for your experiment, but jFed did not receive any candidates." +
                            " Please contact your support team.")
                    .masthead("No projects available")
                    .showError();

        subAuthFetchProgressBar.setVisible(false);

        refreshSubAuthButton.setDisable(false);
    }

    private void disableSubAuthoritiesList() {
        subAuthLabel.setDisable(true);
        subAuthComboBox.getSelectionModel().clearSelection();
        subAuthComboBox.getItems().clear();
        subAuthComboBox.setDisable(true);

        subAuthFetchProgressBar.setVisible(false);

        refreshSubAuthButton.setDisable(true);
    }

    private void enableSubAuthoritiesList() {
        subAuthLabel.setDisable(false);

        if (shouldRefreshSubAuthorities()) {
            refreshSubAuthoritiesList();
        } else {
            showSubAuthoritiesList();
        }
    }

    private boolean shouldRefreshSubAuthorities() {
        ListSubAuthoritiesTask listSubAuthoritiesTask =
                hltf.listSubAuthorities();
        return listSubAuthoritiesTask.newExecutionNeeded();
    }

    @FXML
    private void refreshSubAuthoritiesList() {
        subAuthComboBox.setItems(FXCollections.emptyObservableList());
        subAuthComboBox.setDisable(true);

        subAuthFetchProgressBar.setVisible(true);

        refreshSubAuthButton.setDisable(true);

        //initialize task to fetch sub-authority names
        ListSubAuthoritiesTask listSubAuthoritiesTask = hltf.listSubAuthorities();

        listSubAuthoritiesTask.addCallback((task, taskExecution, state) -> Platform.runLater(this::showSubAuthoritiesList));

        hltf.submitTask(listSubAuthoritiesTask);
    }

    @FXML
    private void showExistingSlicesButton() {
        Set<String> activeSliceNames = slices.keySet();

        if (!activeSliceNames.isEmpty()) {
            ChoiceDialog<String> existingSlicesDialog = new ChoiceDialog<>();
            existingSlicesDialog.setTitle("Select an existing slice");
            existingSlicesDialog.setHeaderText("Select an existing slice");
            existingSlicesDialog.getItems().addAll(activeSliceNames);

            existingSlicesDialog.showAndWait().ifPresent(nameTextField::setText);
        } else {

            JFDialogs.create()
                    .masthead("No slices found")
                    .title("No slices found")
                    .message("You currently don't have any active slices to select from. Please create a new slice to continue.")
                    .showInformation();
        }

    }

    public ValidationSupport getValidationSupport() {
        return validationSupport;
    }

    public void setMaximumSliceNameLength(int maximumSliceNameLength) {
        this.maximumSliceNameLength = maximumSliceNameLength;
    }

    public String getSliceName() {
        return nameTextField.getText();
    }

    public String getSubAuthName() {
        return subAuthCheckBox.isSelected() ? subAuthComboBox.getValue() : null;
    }

    public Instant getExpirationTime() {
        if (daysComboBox.getSelectionModel().isEmpty() ||
                hoursComboBox.getSelectionModel().isEmpty() ||
                minutesComboBox.getSelectionModel().isEmpty())
            return null;


        return ZonedDateTime.now()
                .plusDays(daysComboBox.getSelectionModel().getSelectedItem())
                .plusHours(hoursComboBox.getSelectionModel().getSelectedItem())
                .plusMinutes(minutesComboBox.getSelectionModel().getSelectedItem())
                .plusSeconds(15).toInstant();
    }

    public boolean isIncludeSshKeys() {
        return includeSshKeys.isSelected();
    }

    public boolean isRequestSla() {
        return requestSlaCheckBox.isSelected();
    }

    public TextField getNameTextField() {
        return nameTextField;
    }
}
