/**
 * 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.Version;
import fr.cenra.rhomeo.api.data.ReferenceDescription;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.DashboardResultItem;
import fr.cenra.rhomeo.api.result.IndicatorValuesByYearItem;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.result.ResultStorage;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Function;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.geometry.HPos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.geotoolkit.gui.javafx.util.TaskManager;

/**
 * Table column for indicators.
 * Column present in the {@link FXDashboardResultsTable}.
 *
 * @author Cédric Briançon (Geomatys)
 */
public class IndicatorSiteColumn extends TableColumn<IndicatorValuesByYearItem, Double> implements Comparable<IndicatorSiteColumn> {

    private final Indicator indicator;

    private final ChangeListener<ObservableList> itemPropertyChanged;

    private final InvalidationListener referenceCheckTrigger;

    private final ObjectProperty<Task> checkTaskProperty;

    /**
     * HACK : Cannot find any property to put graphic right to text in a column
     * header, so we put a label in the header to contain both.
     */
    private final Label header;

    public IndicatorSiteColumn(final Indicator indicator) {
        this.indicator = indicator;
        header = new Label(indicator.getName());
        header.setContentDisplay(ContentDisplay.RIGHT);
        setGraphic(header);

        itemPropertyChanged = this::itemPropertyChanged;
        referenceCheckTrigger = obs -> checkReferences();

        tableViewProperty().addListener(this::tableChanged);
        tableViewProperty().addListener(referenceCheckTrigger);

        checkTaskProperty = new SimpleObjectProperty<>();
        checkTaskProperty.addListener((obs, oldVal, newVal) -> {
            if (oldVal != null && oldVal.isRunning())
                oldVal.cancel();
            if (newVal != null && !newVal.isRunning())
                TaskManager.INSTANCE.submit(newVal);

        });
    }

    public Indicator getIndicator() {
        return indicator;
    }

    @Override
    public int compareTo(IndicatorSiteColumn o) {
        if (o == null) {
            return -1;
        }

        if (indicator == null) {
            return 1;
        }
        return indicator.compareTo(o.getIndicator());
    }

    /**
     * Update listeners when parent table changes.
     * @param obs
     * @param oldVal
     * @param newVal
     */
    private void tableChanged(final ObservableValue<? extends TableView> obs, TableView oldVal, TableView newVal) {
        if (oldVal != null) {
            oldVal.itemsProperty().removeListener(itemPropertyChanged);
            oldVal.itemsProperty().removeListener(referenceCheckTrigger);
        }

        if (newVal != null) {
            newVal.itemsProperty().addListener(itemPropertyChanged);
            newVal.itemsProperty().addListener(referenceCheckTrigger);
        }
    }

    /**
     * Update listeners when parent table items change.
     * @param obs
     * @param oldVal
     * @param newVal
     */
    private void itemPropertyChanged(final ObservableValue<? extends ObservableList> obs, ObservableList oldVal, ObservableList newVal) {
        if (oldVal != null)
            oldVal.removeListener(referenceCheckTrigger);

        if (newVal != null)
            newVal.addListener(referenceCheckTrigger);
    }

    /**
     * Browse column indices to find all reference versions used for their computing.
     * If we find multiple versions for a same reference, we will display a little
     * warning to the user.
     */
    private void checkReferences() {
        final ObservableList<IndicatorValuesByYearItem> items;
        if (getTableView() == null || (items = getTableView().getItems()) == null || items.isEmpty())
            return;

        final Site site;
        if (getTableView() instanceof FXDashboardResultsTable)
            site = ((FXDashboardResultsTable)getTableView()).siteProperty().get();
        else
            site = null;

        if (site == null)
            return;

        final ReferenceChecker checker = new ReferenceChecker(site, items);
        checker.setOnSucceeded(evt -> Platform.runLater(() -> {
            final Map<ReferenceDescription, Map<Version, Set<Integer>>> md = checker.getValue();
            if (multipleVersionsInUse(md)) {
                final ImageView imgView = new ImageView(Rhomeo.ICON_WARNING_ORANGE);
                final Tooltip tt = new Tooltip("Les versions de référentiels ne sont pas homogènes.");
                tt.setContentDisplay(ContentDisplay.BOTTOM);
                tt.getStyleClass().add("info-popup");
                tt.setGraphic(createConflictView(md));
                final Label label = new Label(null, imgView);
                label.setTooltip(tt);
                header.setGraphic(label);
            }
        }));

        checker.setOnFailed(evt -> Platform.runLater(() -> {
            // TODO. For now, do as if nothing happened
        }));

        checkTaskProperty.set(checker);
    }

    private VBox createConflictView(final Map<ReferenceDescription, Map<Version, Set<Integer>>> md) {
        final VBox vbox = new VBox(10);
        vbox.setFillWidth(true);
        vbox.setStyle("font-size: 12pt;");
        for (Map.Entry<ReferenceDescription, Map<Version, Set<Integer>>> entry : md.entrySet()) {
            if (entry.getValue().size() < 2)
                continue;
            final BorderPane bp = new BorderPane();
            final Label title = new Label(entry.getKey().getTitle());
            title.getStyleClass().add("section-title");
            bp.setTop(title);
            final GridPane gp = new GridPane();
            bp.setCenter(gp);
            gp.getColumnConstraints().addAll(
                    new ColumnConstraints(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE, Priority.NEVER, HPos.LEFT, true),
                    new ColumnConstraints(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE, Priority.ALWAYS, HPos.CENTER, true)
            );
            final Map<Version, Set<Integer>> vMap = entry.getValue();
            int rowIndex = 0;
            for (Map.Entry<Version, Set<Integer>> vEntry : vMap.entrySet()) {
                gp.add(new Label(vEntry.getKey().toString()), 0, rowIndex);
                final StringJoiner sj = new StringJoiner(", ");
                vEntry.getValue().forEach(val -> sj.add(String.valueOf(val)));
                gp.add(new Label(sj.toString()), 1, rowIndex++);
            }
            vbox.getChildren().add(bp);
        }

        return vbox;
    }

    private boolean multipleVersionsInUse(final Map<ReferenceDescription, Map<Version, Set<Integer>>> metadata) {
        for (final Map.Entry<ReferenceDescription, Map<Version, Set<Integer>>> entry : metadata.entrySet()) {
            if (entry.getValue().size() > 1)
                return true;
        }
        return false;
    }

    /**
     * Read metadata associated to given items and column indicator, to find all
     * versions used for references.
     */
    private class ReferenceChecker extends Task<Map<ReferenceDescription, Map<Version, Set<Integer>>>> {

        final List<IndicatorValuesByYearItem> items;
        final Site site;

        ReferenceChecker(final Site site, final List<IndicatorValuesByYearItem> items) {
            this.site = site;
            this.items = items;
        }

        @Override
        protected Map<ReferenceDescription, Map<Version, Set<Integer>>> call() throws Exception {
            final Session session = Session.getInstance();
            final ResultStorage storage = session.getBean(ResultStorage.class);

            final Map<ReferenceDescription, Map<Version, Set<Integer>>> refVersions = new HashMap<>();
            for (final IndicatorValuesByYearItem item : items) {
                if (isCancelled())
                    break;
                final IndicatorValuesByYearItem.PublishedValue tmpVal = item.getValueForIndicator(indicator.getName());
                if (tmpVal != null) {
                    final DashboardResultItem tmpItem = new DashboardResultItem(item.getYear(), site.getName(), indicator.getName(), tmpVal.getValue(), tmpVal.isPublished());
                    storage.readMetadata(tmpItem)
                            .map(sc -> sc.toDataContext(session))
                            .map(ctx -> session.transform(ctx.getReferences()))
                            .ifPresent(map -> merge(map, refVersions, item.getYear()));
                }
            }

            return refVersions;
        }

        void merge(final Map<ReferenceDescription, Version> source, final Map<ReferenceDescription, Map<Version, Set<Integer>>> dest, int year) {
            for (final Map.Entry<ReferenceDescription, Version> entry : source.entrySet()) {
                dest.computeIfAbsent(entry.getKey(), MAP_CREATOR)
                        .computeIfAbsent(entry.getValue(), SET_CREATOR)
                        .add(year);
            }
        }
    }

    private static final Function<Version, Set<Integer>> SET_CREATOR = input -> new HashSet<>();
    private static final Function<ReferenceDescription, Map<Version, Set<Integer>>> MAP_CREATOR = input -> new HashMap<>();
}
