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

import be.iminds.ilabt.jfed.experimenter_gui.tasks.FetchAllSlicesInfoTask;
import be.iminds.ilabt.jfed.experimenter_gui.tasks.TasksFactory;
import be.iminds.ilabt.jfed.experimenter_gui.ui.progressbar.TaskService;
import be.iminds.ilabt.jfed.highlevel.controller.Task;
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.ui.javafx.GlyphUtils;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.util.Callback;
import org.controlsfx.glyphfont.FontAwesome;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * User: twalcari
 * Date: 12/5/13
 * Time: 9:21 PM
 */
public class RecoverSliceController {
    private static final Logger LOG = LoggerFactory.getLogger(RecoverSliceController.class);
    private static final DateTimeFormatter EXPIRATION_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    private static final String ALL_PROJECTS = "All projects";

    @FXML
    private TextField nameFilterTextField;
    @FXML
    private ComboBox<String> projectFilterComboBox;

    @FXML
    private TableView<Slice> slicesTableView;

    @FXML
    private TableColumn<Slice, Boolean> sliceHasSliversTableColumn;
    @FXML
    private TableColumn<Slice, String> sliceNameTableColumn;
    @FXML
    private TableColumn<Slice, String> subAuthTableColumn;
    @FXML
    private TableColumn<Slice, Instant> expirationTableColumn;
    @FXML
    private HBox initializingBox;

    @FXML
    private ProgressIndicator refreshProgressIndicator;
    @FXML
    private Label refreshLabel;

    private final SfaModel sfaModel;
    private final HighLevelTaskFactory hltf;
    private final TasksFactory tasksFactory;
    private final TaskService taskService;

    private Consumer<MouseEvent> onDoubleClickEvent = null;

    private final EventHandler<MouseEvent> mouseEventEventHandler = mouseEvent -> {
        if (mouseEvent.getClickCount() == 2) {
            if (onDoubleClickEvent != null)
                onDoubleClickEvent.accept(mouseEvent);
        }
    };


    @Inject
    RecoverSliceController(SfaModel sfaModel, HighLevelTaskFactory hltf, TasksFactory tasksFactory, TaskService taskService) {
        this.sfaModel = sfaModel;
        this.hltf = hltf;
        this.tasksFactory = tasksFactory;
        this.taskService = taskService;

    }

    @FXML
    public void initialize() {
        SortedList<Slice> sortedSlices =
                new SortedList<>(this.sfaModel.getSlices(), Comparator.comparing(Slice::getHasActiveSlivers,
                        Comparator.nullsLast(Comparator.naturalOrder())
                      /*  ,(sliceHasSliversA, sliceHasSliversB) -> {
                            //always show slices with active slivers first
                            if (sliceHasSliversA == Boolean.TRUE && sliceHasSliversB != Boolean.TRUE)
                                return -1;
                            if (sliceHasSliversA != Boolean.TRUE && sliceHasSliversB == Boolean.TRUE)
                                return 1;

                            return 0;
                        }*/).thenComparing(Slice::getName, String.CASE_INSENSITIVE_ORDER));

        FilteredList<Slice> filteredAndSortedSlices = new FilteredList<>(sortedSlices);
        slicesTableView.setItems(filteredAndSortedSlices);

        sliceNameTableColumn.setCellValueFactory(param -> new ReadOnlyStringWrapper(param.getValue().getName()));
        sliceNameTableColumn.setCellFactory(sliceStringTableColumn -> new SliceStringTableCell());
        sliceHasSliversTableColumn.setCellValueFactory(t -> t.getValue().hasActiveSliversProperty());
        sliceHasSliversTableColumn.setCellFactory(new Callback<TableColumn<Slice, Boolean>, TableCell<Slice, Boolean>>() {
            @Override
            public TableCell<Slice, Boolean> call(TableColumn<Slice, Boolean> sliceStringTableColumn) {
                return new TableCell<Slice, Boolean>() {
                    {
                        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                        setAlignment(Pos.CENTER);
                        setOnMouseClicked(mouseEventEventHandler);
                    }

                    @Override
                    protected void updateItem(Boolean hasActiveSlivers, boolean empty) {
                        super.updateItem(hasActiveSlivers, empty);
                        if (empty) {
                            setGraphic(null);
                        } else if (hasActiveSlivers == Boolean.FALSE) {
                            setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.TIMES, Color.DARKGRAY));
                        } else if (hasActiveSlivers == Boolean.TRUE) {
                            setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.CHECK_SQUARE, Color.GREEN));
                        } else if (hasActiveSlivers == null) {
                            setGraphic(GlyphUtils.createGlyph(FontAwesome.Glyph.QUESTION_CIRCLE, Color.BLACK));
                        }
                    }
                };
            }
        });
        subAuthTableColumn.setCellValueFactory(t -> new SimpleStringProperty(t.getValue().getSubAuthorityName()));
        subAuthTableColumn.setCellFactory(param -> new SliceStringTableCell());

        expirationTableColumn.setCellValueFactory(param -> param.getValue().expirationDateProperty());
        expirationTableColumn.setCellFactory(new Callback<TableColumn<Slice, Instant>, TableCell<Slice, Instant>>() {
            @Override
            public TableCell<Slice, Instant> call(TableColumn<Slice, Instant> param) {
                return new TableCell<Slice, Instant>() {
                    @Override
                    protected void updateItem(Instant item, boolean empty) {
                        super.updateItem(item, empty);

                        final Slice slice = (Slice) getTableRow().getItem();

                        if (slice != null) {
                            if (item != null) {
                                setGraphic(null);
                                setText(EXPIRATION_TIME_FORMATTER.format(item.atZone(ZoneId.systemDefault())));

                                Duration duration = Duration.between(Instant.now(), item);

                                if (duration.isNegative()) {
                                    setTextFill(Color.RED);
                                } else if (duration.toHours() < 12) {
                                    setTextFill(Color.ORANGERED);
                                } else if (duration.toHours() < 24) {
                                    setTextFill(Color.ORANGE);
                                } else if (duration.toDays() < 7) {
                                    setTextFill(Color.GREEN);
                                } else {
                                    setTextFill(Color.DARKGREEN);
                                }

                                LOG.debug("Setting expiration time of {} to {}", slice.getName(), item);
                            } else {
                                final ProgressIndicator progressIndicator = new ProgressIndicator(-1);
                                progressIndicator.setPrefSize(20, 20);
                                setGraphic(progressIndicator);
                                setText(null);

                                LOG.debug("No expiration time available for {}", slice.getName());
                            }
                        } else {
                            setGraphic(null);
                            setText(null);
                        }
                    }
                };
            }
        });

        DoubleBinding safeWidth = slicesTableView.widthProperty().subtract(3);
        sliceHasSliversTableColumn.prefWidthProperty().bind(safeWidth.multiply(0.1));
        sliceNameTableColumn.prefWidthProperty().bind(safeWidth.multiply(0.4));
        subAuthTableColumn.prefWidthProperty().bind(safeWidth.multiply(0.15));
        expirationTableColumn.prefWidthProperty().bind(safeWidth.multiply(0.35));

        //apply filters when necessary

        InvalidationListener filterListener = observable -> {

            String nameFilterText = nameFilterTextField.getText().trim().toLowerCase();
            String selectedProject = projectFilterComboBox.getSelectionModel().getSelectedItem();

            filteredAndSortedSlices.setPredicate(slice ->
                    (nameFilterText.isEmpty()
                            || slice.getName().toLowerCase().contains(nameFilterText))
                            &&
                            (selectedProject == null
                                    || Objects.equals(ALL_PROJECTS, selectedProject)
                                    || Objects.equals(selectedProject, slice.getSubAuthorityName())));
        };


        nameFilterTextField.textProperty().addListener(filterListener);
        projectFilterComboBox.getSelectionModel().selectedItemProperty().addListener(filterListener);


        sfaModel.getLoggedInUserSubAuthorityNames().addListener((InvalidationListener) obs -> {
            LOG.info("Subauthoritynames was updated!");
            updateProjectsList();
        });
        sfaModel.getSlices().addListener((InvalidationListener) obs -> {
            LOG.info("Slices was updated! Now {} items", sfaModel.getSlices().size());
            updateProjectsList();
        });

        updateProjectsList();

        //show loading progressview when initializing
        javafx.concurrent.Task loadingTask = new javafx.concurrent.Task<Void>() {
            final Task resolveTask = hltf.resolveUserAndSlices();

            @Override
            protected Void call() throws Exception {
                while (resolveTask.isActive()) {
                    Thread.sleep(100);
                }

                //check if some resolveSlice-calls are still running
                for (Slice slice : RecoverSliceController.this.sfaModel.getSlices()) {
                    while (hltf.getAggregatesForSlice(slice).isActive())
                        Thread.sleep(100);
                }
                return null;
            }

            @Override
            protected void succeeded() {
                initializingBox.setVisible(false);
            }
        };

        final Thread thread = new Thread(loadingTask);
        thread.setDaemon(true);
        thread.setName("Recover-Loading-Task");
        thread.start();
    }

    private void updateProjectsList() {

        ObservableList<String> projects = FXCollections.observableArrayList();
        projects.add(ALL_PROJECTS);

        projects.addAll(sfaModel.getLoggedInUserSubAuthorityNames().stream().sorted().collect(Collectors.toList()));

        projectFilterComboBox.setItems(projects);

        LOG.info("Updating project list with {} projects", projects.size());

        if (projectFilterComboBox.getSelectionModel().isEmpty()) {
            projectFilterComboBox.getSelectionModel().select(ALL_PROJECTS);
        }
    }

    public void refreshSlicesList() {
        refreshProgressIndicator.visibleProperty().unbind();
        refreshLabel.textProperty().unbind();

        FetchAllSlicesInfoTask fetchAllSlicesInfoTask = tasksFactory.createFetchAllSlicesInfoTask();

        refreshProgressIndicator.visibleProperty().bind(fetchAllSlicesInfoTask.runningProperty());
        refreshLabel.textProperty().bind(fetchAllSlicesInfoTask.messageProperty());
        taskService.submitTask(fetchAllSlicesInfoTask);

    }

    private static class IsValueKnownPaintBinding extends ObjectBinding<Paint> {

        private final ObjectProperty<Boolean> isKnownProperty;

        private IsValueKnownPaintBinding(ObjectProperty<Boolean> isKnownProperty) {
            this.isKnownProperty = isKnownProperty;
            bind(isKnownProperty);
        }

        @Override
        protected Paint computeValue() {
            if (isKnownProperty.get() == Boolean.FALSE) {
                return Color.GRAY;
            } else {
                return Color.BLACK;
            }
        }
    }

    private class SliceStringTableCell extends TableCell<Slice, String> {

        {
            setOnMouseClicked(mouseEventEventHandler);
        }

        @Override
        protected void updateItem(String s, boolean b) {
            textFillProperty().unbind();

            if (s != null) {
                setText(s);

            } else {
                setText("");
            }

            if(getTableRow().getItem() != null){
                assert getTableRow().getItem() instanceof Slice;

                textFillProperty().bind(
                        new IsValueKnownPaintBinding(((Slice)getTableRow().getItem()).hasActiveSliversProperty()));

            }
        }
    }

    public ReadOnlyObjectProperty<Slice> selectedSliceProperty() {
        return slicesTableView.getSelectionModel().selectedItemProperty();
    }

    public Slice getSelectedSlice() {
        return slicesTableView.getSelectionModel().getSelectedItem();
    }

    public Consumer<MouseEvent> getOnDoubleClickEvent() {
        return onDoubleClickEvent;
    }

    public void setOnDoubleClickEvent(Consumer<MouseEvent> onDoubleClickEvent) {
        this.onDoubleClickEvent = onDoubleClickEvent;
    }
}
