/**
 * 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.protocol.p03;

import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.data.TrackingPointFilter;
import fr.cenra.rhomeo.api.ui.FilterTable;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Iterator;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;

/**
 * A simple table to present hydrology data related to tracking points.
 *
 * @author Alexis Manin (Geomatys)
 */
class PointTable extends FilterTable<TrackingPointFilter> {

    /**
     * List of statements present in the session, sorted by date (from older to
     * earliest).
     *
     * /!\ Only dated elements are present in this list !
     */
    final FilteredList<Statement> items;
    final Session session;

    final Dataset dataset;
    final ObservableList<TrackingPoint> filteredPoints;

    final ObservableList<TrackingPointFilter> filters;

    PointTable(final Session session) {
        this.session = session;

        final TableColumn<TrackingPointFilter, String> nameColumn = new TableColumn("Nom");
        nameColumn.setCellValueFactory(input -> new SimpleStringProperty(input.getValue().point == null ? null : input.getValue().point.getName()));

        final TableColumn<TrackingPointFilter, LocalDate> startDateColumn = new TableColumn("Date de début");
        startDateColumn.setCellValueFactory(this::initStartDate);

        final TableColumn<TrackingPointFilter, LocalDate> endDateColumn = new TableColumn("Date de fin");
        endDateColumn.setCellValueFactory(this::initEndDate);

        final TableColumn<TrackingPointFilter, Integer> yearColumn = new TableColumn("Année hydrologique");
        yearColumn.setCellValueFactory(input -> (ObservableValue) new SimpleIntegerProperty(input.getValue().point == null ? null : input.getValue().point.getYear()));

        final TableColumn<TrackingPointFilter, Float> nbColumn = new TableColumn("% données");
        nbColumn.setCellValueFactory(this::initDataCoverage);
        nbColumn.setCellFactory(RhomeoCore.ROUNDING_CELL_FACTORY);

        getColumns().addAll(nameColumn, startDateColumn, endDateColumn, yearColumn, nbColumn);
        setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

        items = ((ObservableList<Statement>) session.getDataset().getItems())
                .filtered(input -> input.getDate() != null)
                .sorted((o1, o2) -> o1.getDate().compareTo(o2.getDate())).filtered(null);
        dataset = new Dataset(session.getDataset().getProtocol());
        dataset.getItems().setAll(items);
        items.addListener(this::synchronizeDataset);
        
        filteredPoints = dataset.getTrackingPoints();
        filters = FXCollections.observableArrayList(filteredPoints.stream().map(input -> new TrackingPointFilter(input)).collect(Collectors.toList()));
        filteredPoints.addListener(this::synchronizeFilters);
        setItems(filters);
    }

    /**
     * Report changes from {@link #items} into internal {@link #dataset}. It
     * allows us to keep an up to date list of tracking points.
     *
     * @param c
     */
    private void synchronizeDataset(final ListChangeListener.Change<? extends Statement> c) {
        while (c.next()) {
            dataset.getItems().removeAll(c.getRemoved());
            dataset.getItems().addAll(c.getAddedSubList());
        }
    }

    /**
     * Report changes from {@link #items} into internal {@link #dataset}. It
     * allows us to keep an up to date list of tracking points.
     *
     * @param c
     */
    private void synchronizeFilters(final ListChangeListener.Change<? extends TrackingPoint> c) {
        while (c.next()) {
            final HashSet<TrackingPoint> set = new HashSet<>(c.getRemoved());
            final Iterator<TrackingPointFilter> it = filters.iterator();
            while (it.hasNext()) {
                if (set.contains(it.next().point))
                    it.remove();
            }

            filters.addAll(c.getAddedSubList().stream().map(input -> new TrackingPointFilter(input)).collect(Collectors.toList()));
        }
    }

    @Override
    public Predicate<? super Statement> getPreFilter() {
        return items.getPredicate();
    }

    @Override
    public void setPreFilter(final Predicate<Statement> filter) {
        items.setPredicate(filter);
    }

    @Override
    public ObjectProperty<Predicate<? super Statement>> preFilterProperty() {
        return items.predicateProperty();
    }

    /**
     * Find and maintain oldest date in the list of statements associated with
     * given tracking point.
     *
     * @param input The cell feature containing target tracking point.
     * @return An observable value which represents latest date on the given
     * tracking point.
     */
    private ObjectBinding<LocalDate> initStartDate(final TableColumn.CellDataFeatures<TrackingPointFilter, LocalDate> input) {
        return Bindings.createObjectBinding(() -> {
            if (items.isEmpty() || input.getValue().point == null) {
                return null;
            } else
                for (final Statement st : items) {
                    if (Dataset.belongToPoint(input.getValue().point, st))
                        return st.getDate();
                }

            return null;
        }, items);
    }

    /**
     * Find and maintain newest date in the list of statements associated with
     * given tracking point.
     *
     * @param input The cell feature containing target tracking point.
     * @return An observable value which represents earliest date on the given
     * tracking point.
     */
    private ObjectBinding<LocalDate> initEndDate(final TableColumn.CellDataFeatures<TrackingPointFilter, LocalDate> input) {
        return Bindings.createObjectBinding(() -> {
            if (items.isEmpty() || input.getValue().point == null) {
                return null;
            } else
                for (int i = items.size() - 1; i > 0; i--) {
                    final Statement st = items.get(i);
                    if (Dataset.belongToPoint(input.getValue().point, st))
                        return st.getDate();
                }

            return null;
        }, items);
    }

    /**
     * Create an observable value which computes percentage of days with data in
     * a given tracking point.
     *
     * @param input The target tracking point.
     * @return An observable value which represents data coverage on the given
     * tracking point.
     */
    public ObjectBinding<Float> initDataCoverage(final TableColumn.CellDataFeatures<TrackingPointFilter, Float> input) {
        return Bindings.createObjectBinding(() -> {
            final TrackingPoint point;
            if (input.getValue() == null || (point = input.getValue().point) == null)
                return null;
            else
                return P03.computeDataCoverage(point, session.getDataset().getItems());

        }, items);
    }
}
