/**
 * Copyright © CENRA (2016)
 *
 * remi.clement@espaces-naturels.fr
 * 
 * This software is a multi-platform and portable application based on 
 * the scientifical toolbox to monitor wetlands. With this toolbox it’s 
 * possible to calculate indicators «RhoMéO» to monitor quality, 
 * fonctionalities and urban and agricultural pressures of wetlands. 
 * Indicators are calculated using naturalist data entered and/or imported,
 * and geographic data downloaded from a geographic server.
 * 
 * This software is governed by the CeCILL license under French law and
 * abiding by the rules of distribution of free software.  You can  use, 
 * modify and/ or redistribute the software under the terms of the CeCILL
 * license as circulated by CEA, CNRS and INRIA at the following URL
 * "http://www.cecill.info". 
 * 
 * As a counterpart to the access to the source code and  rights to copy,
 * modify and redistribute granted by the license, users are provided only
 * with a limited warranty  and the software's author,  the holder of the
 * economic rights,  and the successive licensors  have only  limited
 * liability. 
 * 
 * In this respect, the user's attention is drawn to the risks associated
 * with loading,  using,  modifying and/or developing or reproducing the
 * software by the user in light of its specific status of free software,
 * that may mean  that it is complicated to manipulate,  and  that  also
 * therefore means  that it is reserved for developers  and  experienced
 * professionals having in-depth computer knowledge. Users are therefore
 * encouraged to load and test the software's suitability as regards their
 * requirements in conditions enabling the security of their systems and/or 
 * data to be ensured and,  more generally, to use and operate it in the 
 * same conditions as regards security. 
 * 
 * The fact that you are presently reading this means that you have had
 * knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;

import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.api.data.DataContext;
import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.process.Process;
import fr.cenra.rhomeo.api.process.ProcessContext;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.api.ui.FilterTable;
import fr.cenra.rhomeo.api.ui.FilterTableSpi;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.core.util.SimplePredicateChain;
import fr.cenra.rhomeo.fx.edition.FXTrackingPointTable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;


/**
 *
 * Default JavaFX pane to set processing parameters.
 *
 * For a given {@link Dataset} and a given {@link Protocol}, this pane allows to
 * choose the {@link Indicator}(s) and the {@link TrackingPoint}(s) the processing
 * must be computed for.
 *
 * @author Samuel Andrés (Geomatys)
 *
 * @param <T>
 */
@Component("fr.cenra.rhomeo.fx.FXDefaultProcessingPane")
@Scope("prototype")
public class FXDefaultProcessingPane<T extends Statement> extends VBox implements InternationalResource {

    @FXML private GridPane uiIndicatorPane;
    private final Collection<IndicatorSelection> indicatorSelections = new ArrayList<>();

    @FXML private FlowPane uiYearPane;
    private final Collection<YearSelection> yearSelections = new ArrayList<>();

    @FXML private BorderPane uiTrackingPointPane;

    private FilterTable<? extends Predicate> uiSelectionTable;
    private final TableColumn<Predicate, Boolean> uiSelectionColumn;

    private final Map<Predicate, BooleanProperty> filterSelection = new HashMap<>();
    private final Function<Predicate, BooleanProperty> selectionComputer;

    @FXML
    private VBox uiProgressBox;

    @FXML
    private Label uiProgressTitle;

    @FXML
    private ProgressBar uiProgress;

    @FXML
    private Label uiProgressMessage;

    @FXML
    private HBox uiBtnBox;

    private Dataset<T> dataset;
    private Protocol protocol;
    private ProcessContext processContext;
    private Site site;

    private final ChangeListener<Boolean> selectionListener = this::selectionChanged;
    private final SimpleObjectProperty<Task<Set<Index>>> globalTask = new SimpleObjectProperty<>();

    private SimplePredicateChain processFilter;

    @Autowired
    private Session session;

    @Autowired
    private List<FilterTableSpi> pointTableSpis;

    public FXDefaultProcessingPane() {
        Rhomeo.loadFXML(this, FXDefaultProcessingPane.class);

        uiProgressBox.setVisible(false);
        globalTask.addListener((obs, oldTask, newTask) -> {
            if (oldTask != null) {
                uiProgressBox.visibleProperty().unbind();
                uiProgressBox.setVisible(false);
                uiProgressTitle.textProperty().unbind();
                uiProgressMessage.textProperty().unbind();
                uiProgress.progressProperty().unbind();
                uiIndicatorPane.disableProperty().unbind();
                uiTrackingPointPane.disableProperty().unbind();
                uiBtnBox.disableProperty().unbind();
            } else {
                uiProgressBox.visibleProperty().bind(newTask.runningProperty());
                uiProgressTitle.textProperty().bind(newTask.titleProperty());
                uiProgressMessage.textProperty().bind(newTask.messageProperty());
                uiProgress.progressProperty().bind(newTask.progressProperty());
                uiIndicatorPane.disableProperty().bind(newTask.runningProperty());
                uiTrackingPointPane.disableProperty().bind(newTask.runningProperty());
                uiBtnBox.disableProperty().bind(newTask.runningProperty());
            }
        });

        uiSelectionColumn = new TableColumn(ResourceBundle.getBundle(FXDefaultProcessingPane.class.getCanonicalName()).getString("selectionColumn"));
        selectionComputer = input -> {
            final BooleanProperty bp = new SimpleBooleanProperty(input, "selection", false);
            bp.addListener(selectionListener);
            bp.set(true);

            return bp;
        };
    }

    @PostConstruct
    public void postConstruct() {
        dataset = session.getDataset();
        ArgumentChecks.ensureNonNull("dataset", dataset);

        final DataContext context = session.getDataContext();
        protocol = context.getProtocol();
        ArgumentChecks.ensureNonNull("protocol", protocol);

        site = context.getSite();
        ArgumentChecks.ensureNonNull("site", site);

        /*
        1- Indicator pane.
        ================*/
        final List<Indicator> sortedIndicators = new ArrayList<>(session.getIndicators());
        sortedIndicators.removeIf(i -> !protocol.equals(i.getProtocol()));
        Collections.sort(sortedIndicators);
        initIndicatorPane(sortedIndicators);

        /*
        2- Year pane.
        ============*/
        final List<Integer> years = new ArrayList<>();
        for(final TrackingPoint trackingPoint : dataset.getTrackingPoints()){
            final int year = trackingPoint.getYear();

            // Do not add the same year twice…
            if(!years.contains(year)){
                years.add(year);
            }
        }

        // Sort years…
        Collections.sort(years);
        initYearPane(years);

        /*
        3- Tracking point pane.
        =====================*/
        initTrackingPointPane();

        /*
        4- LISTENERS.
        =====================*/
        uiSelectionTable.getItems().addListener(this::tableContentChanged);
        for (final IndicatorSelection is : indicatorSelections) {
            is.selectedProperty.addListener(selectionListener);
        }

        // FXTrackingPointPane must only display tracking point which year is selected into FXYearPane
        yearSelections.forEach(yearSelection ->
            yearSelection.selectedProperty().addListener(selectionListener)
        );

        /*
        4- RESTORE PROCESS CONTEXT.
        =====================*/
        // Start by extracting previous indicators / years / filters
        processContext = session.getProcessContext();
        final Set<Indicator> previousIndics = new HashSet<>(processContext.getIndicators());
        final HashSet<Integer> previousYears = new HashSet<>();
        processContext.getTrackingPoints().forEach(tp -> previousYears.add(tp.getYear()));
        if (processContext.getFilter() instanceof SimplePredicateChain)
            processFilter = (SimplePredicateChain) processContext.getFilter();
        else {
            processFilter = new SimplePredicateChain();
            if (processContext.getFilter() != null)
                processFilter.predicates.add(processContext.getFilter());
            processContext.setFilter(processFilter);
        }
        final Set<Predicate> previousFilters = new HashSet(processFilter.predicates);

        // Restore selected indicators.
        for (final IndicatorSelection is : indicatorSelections)
            is.selectedProperty.set(previousIndics.contains(is.indicator));

        // Find activated years
        for (final YearSelection ys : yearSelections)
            ys.selectedProperty.set(previousYears.contains(ys.year));

        // Set same filter selection
        if (!previousFilters.isEmpty())
            for (final Predicate p : uiSelectionTable.getItems())
                filterSelection.computeIfAbsent(p, selectionComputer).set(previousFilters.contains(p));
    }

    /**
     *
     * @return The selected {@link Indicator}s.
     */
    public Collection<Indicator> getIndicators() {
        return indicatorSelections.stream()
                .filter(is -> is.selectedProperty().get())
                .map(IndicatorSelection::getIndicator)
                .collect(Collectors.toList());
    }

    ////////////////////////////////////////////////////////////////////////////
    // indicator pane // indicator pane // indicator pane // indicator pane

    /**
     * Init sub-pane for displaying indicators.
     */
    private void initIndicatorPane(final List<Indicator> indicators){

        for(int i=0; i<indicators.size(); i++){
            final Indicator indicator = indicators.get(i);
            final IndicatorSelection indicatorSelection = new IndicatorSelection(indicator);
            indicatorSelections.add(indicatorSelection);
            final CheckBox checkBox = new CheckBox();
            checkBox.setAllowIndeterminate(false);
            final Label label = new Label();
            label.setText(indicator.getRemarks()+" - "+indicator.getProtocol().getRemarks());
            indicatorSelection.selectedProperty.bindBidirectional(checkBox.selectedProperty());
            final Label labelButton = new Label(indicator.getName());
            labelButton.getStyleClass().add(Rhomeo.CSS_INDICATOR_LABEL);

            if(!indicator.getZoneTypeCodes().contains(site.getZoneType())){
                checkBox.setSelected(false);
                checkBox.setDisable(true);
                labelButton.setDisable(true);
                final Label alert = new Label(null, new ImageView(Rhomeo.ICON_WARNING_ORANGE));
                alert.setTooltip(new Tooltip("Cet indicateur n'est pas compatible avec le type de zone humide du site."));
                uiIndicatorPane.addRow(i, checkBox, labelButton, label, alert);
            }
            else{
                uiIndicatorPane.addRow(i, checkBox, labelButton, label);
            }
        }
    }

    /**
     * A simple class associating a selection boolean state to an {@link Indicator}.
     */
    private static class IndicatorSelection extends Selection {

        private final Indicator indicator;

        public IndicatorSelection(final Indicator indicator){
            ArgumentChecks.ensureNonNull("indicator", indicator);
            this.indicator = indicator;
        }

        public Indicator getIndicator(){return indicator;}
    }


    ////////////////////////////////////////////////////////////////////////////
    // year pane // year pane // year pane // year pane // year pane // year pane

    /**
     * Init sub-pane for displaying years.
     *
     * This pane is an immutable FlowPane whose children represent the state
     * (selected or not) of years given to the constructor. The state of the
     * years is accessible with {@link YearSelection#selectedProperty()}.
     */
    private void initYearPane (final List<Integer> years){
        for (final Integer year : years) {
            final YearSelection yearSelection = new YearSelection(year);
            yearSelections.add(yearSelection);
            final CheckBox checkBox = new CheckBox();
            checkBox.setAllowIndeterminate(false);
            final Label label = new Label();
            label.setText(year.toString());
            yearSelection.selectedProperty.bindBidirectional(checkBox.selectedProperty());
            uiYearPane.getChildren().add(new HBox(checkBox, label));
        }
    }

    /**
     * A simple class associating a selection boolean state to a year.
     */
    private static class YearSelection extends Selection {

        private final int year;

        public YearSelection(final int year){
            this.year = year;
        }

        public int getYear(){return year;}
    }


    ////////////////////////////////////////////////////////////////////////////
    // trackingPoint pane // trackingPoint pane // trackingPoint pane

    /**
     * Init sub-pane for displaying {@link TrackingPoint}s.
     */
    private void initTrackingPointPane(){
        for (final FilterTableSpi spi : pointTableSpis) {
            if (spi.getProtocol() == session.getDataContext().getProtocol()) {
                uiSelectionTable = spi.createEmptyTable();
                break;
            }
        }

        if (uiSelectionTable == null) {
            uiSelectionTable = new FXTrackingPointTable(session);
        }

        uiSelectionTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        uiSelectionTable.getColumns().add(0, (TableColumn) uiSelectionColumn);
        uiSelectionTable.setPrefSize(USE_COMPUTED_SIZE, USE_COMPUTED_SIZE);

        uiSelectionColumn.setCellValueFactory(param -> filterSelection.computeIfAbsent(param.getValue(), selectionComputer));
        uiSelectionColumn.setCellFactory(param -> {
                    final TableCell<Predicate, Boolean> cell = new CheckBoxTableCell<>();
                    cell.setEditable(true);
                    return cell;
                });
        uiSelectionColumn.setEditable(true);
        uiSelectionTable.setEditable(true);
        uiSelectionTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        uiTrackingPointPane.setCenter(uiSelectionTable);
    }

    /**
     * Filter dataset tracking points according to current year selection, to set
     * {@link #uiTrackingPointPane} content. If no year is selected, all points
     * are activated.
     */
    private void resetSelection() {
        uiSelectionTable.getSelectionModel().clearSelection();
        final Set<Integer> selectedYears = yearSelections.stream()
                .filter(ys -> ys.selectedProperty.get())
                .map(ys -> ys.year)
                .collect(Collectors.toSet());

        if (selectedYears.isEmpty()) {
            uiSelectionTable.setPreFilter((input) -> false);
        } else if (selectedYears.size() == yearSelections.size()) {
            uiSelectionTable.setPreFilter(null);
        } else {
            final Function<TrackingPoint, Boolean> yearChecker = tp -> selectedYears.contains(tp.getYear());
            final Predicate<Statement> preFilter = st -> {
                return dataset.findPoint(st)
                        .map(yearChecker)
                        .orElse(false);
            };
            uiSelectionTable.setPreFilter(preFilter);
        }
    }

    private void tableContentChanged(final ListChangeListener.Change<? extends Predicate> c) {
        while (c.next()) {
            if (c.wasRemoved()) {
                for (final Predicate p : c.getRemoved()) {
                    filterSelection.computeIfAbsent(p, selectionComputer).set(false);
                }
            }

            if (c.wasAdded()) {
                for (final Predicate p : c.getAddedSubList()) {
                    filterSelection.computeIfAbsent(p, selectionComputer).set(true);
                }
            }
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    // Utilities // Utilities // Utilities // Utilities // Utilities //

    /**
     * Utility abstract class providing a selection boolean state.
     */
    private static abstract class Selection {
        protected final BooleanProperty selectedProperty = new SimpleBooleanProperty(this, "selection", false);

        public ReadOnlyBooleanProperty selectedProperty(){
            return selectedProperty;
        }
    }

    /**
     * Update UI/related components when either year, indicator or filter
     * selection change.
     *
     * @param obs Observable selection which change. For this method to actually
     * do something, the given observable must be a {@link ReadOnlyProperty}
     * with non-null {@link ReadOnlyProperty#getBean() } property.
     * @param oldValue
     * @param newValue
     */
    private void selectionChanged(final ObservableValue<? extends Boolean> obs, Boolean oldValue, Boolean newValue) {
        final Object bean;
        if (obs instanceof ReadOnlyProperty) {
            bean = ((ReadOnlyProperty)obs).getBean();
        } else {
            bean = null;
        }

        if (bean instanceof YearSelection) {
            // Synchronize tracking points state with corresponding year.
            resetSelection();

        } else if (bean instanceof IndicatorSelection) {
                final Indicator i = ((IndicatorSelection)bean).getIndicator();
            if (Boolean.TRUE.equals(newValue)) {
                processContext.getIndicators().add(i);
            } else {
                processContext.getIndicators().remove(i);
            }
        } else if (bean instanceof Predicate) {
            if (Boolean.TRUE.equals(newValue)) {
                processFilter.predicates.add((Predicate) bean);
            } else {
                processFilter.predicates.remove((Predicate) bean);
            }
            processContext.setFilter(null);
            processContext.setFilter(processFilter);
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    // Process management // Process management // Process management //

    @FXML
    void nextStep(ActionEvent event) {
        final Computer computer = new Computer();
        globalTask.set(computer);

        computer.stateProperty().addListener(obs -> {
            if (computer.isDone()) {
                switch (computer.getState()) {
                    case FAILED:
                        Rhomeo.showAlert(Alert.AlertType.ERROR, getFormatedResourceString("process-failed"));
                        break;
                    case SUCCEEDED:
                        session.setResults(computer.getValue());
                        globalTask.set(null);
                        session.requestWorkflowStep(WorkflowStep.RESULT);
                        break;
                }
            }
        });

        TaskManager.INSTANCE.submit(computer);
    }

    @FXML
    void cancelProcess(ActionEvent event) {
        final Task<Set<Index>> task = globalTask.get();
        if (task != null)
            task.cancel(true);
    }

    /**
     * Wrap processes generated by requested indicators, execute them and wait
     * for completion of the entire process set.
     */
    private class Computer extends Task<Set<Index>> {

        private final List<Process> processes = new ArrayList<>();

        @Override
        protected Set<Index> call() throws Exception {
            updateProgress(-1, -1);
            updateTitle(getResourceString("process.prepare"));
            for (final Indicator indicator : getIndicators()) {
                processes.addAll(indicator.createProcesses(processContext));
            }

            session.getAdditionalValues().clear();
            if (processes.size() < 1) {
                throw new IllegalStateException("Nothing to process.");

            } else if (processes.size() == 1) {
                final Process wrapped = processes.get(0);
                Platform.runLater(() -> {
                    wrapped.titleProperty().addListener((obs) -> updateTitle(wrapped.getTitle()));
                    wrapped.messageProperty().addListener((obs) -> updateTitle(wrapped.getMessage()));
                    wrapped.workDoneProperty().addListener((obs) -> updateProgress(wrapped.getWorkDone(), wrapped.getTotalWork()));
                });

                wrapped.run();
                return (Set) wrapped.get();

            } else {
                /*
                 * Compute a maximal progress for this process aggregator.
                 * If any process has no total work defined, the aggregator total is
                 * the number of processes to execute. Otherwise, its the sum of all
                 * process to execute.
                 *
                 * The same is done for the "work done" property. If the aggregated
                 * total is undefined, the work done is the number of finished
                 * processes. Otherwise, it's the sum of work done of each process.
                 */
                final Observable[] totals = new Observable[processes.size()];
                final Observable[] worksDone = new Observable[processes.size()];
                for (int i = 0; i < processes.size(); i++) {
                    totals[i] = processes.get(i).totalWorkProperty();
                    worksDone[i] = processes.get(i).workDoneProperty();
                }

                final DoubleBinding totalBinding = Bindings.createDoubleBinding(() -> {
                    double result = 0;
                    double processTotal;
                    for (final Process p : processes) {
                        processTotal = p.getTotalWork();
                        if (processTotal < 0) {
                            return (double) totals.length;
                        }
                        result += processTotal;
                    }

                    return result;
                }, totals);

                final DoubleBinding workDoneBinding = Bindings.createDoubleBinding(() -> {
                    double result = 0;
                    double processWork;
                    final boolean doCount = totalBinding.get() < 0;
                    for (final Process p : processes) {
                        if (doCount) {
                            if (p.isDone())
                                result++;
                        } else {
                            processWork = p.getWorkDone();
                            if (processWork >= 0) {
                                result += processWork;
                            }
                        }
                    }

                    return result;
                }, worksDone);

                Platform.runLater(() -> {
                    workDoneBinding.addListener((obs) -> updateProgress(workDoneBinding.get(), totalBinding.get()));
                });

                final Set<Index> indices = new HashSet<>();
                updateTitle(getResourceString("process.go"));

                for (final Process p : processes) {
                    if (Thread.interrupted()) {
                        throw new InterruptedException();
                    }

                    TaskManager.INSTANCE.submit(p);
                }

                for (final Process p : processes) {
                    if (Thread.interrupted()) {
                        throw new InterruptedException();
                    }

                    indices.addAll(p.get());
                }

                return indices;
            }
        }

        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            for (final Process p : processes) {
                p.cancel(true);
            }
            return super.cancel(mayInterruptIfRunning);
        }
    }
}
