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

import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.api.ui.FilterTable;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.protocol.p01.P01;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.transformation.FilteredList;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.util.Callback;

/**
 * A simple table whose aim is to display tracking point information.
 * @author Alexis Manin (Geomatys)
 */
public class FXTrackingPointTable extends FilterTable<ByNameAndDateFilter> {

    private static final Callback<TableColumn<ByNameAndDateFilter, LocalDate>, TableCell<ByNameAndDateFilter, LocalDate>> DATE_CELL_FACTORY = FXTrackingPointTable::createDateCell;

    private final Session session;
    private final TableColumn<ByNameAndDateFilter, Integer> nbColumn;

    private final FilteredList<Statement> dataset;

    public FXTrackingPointTable(final Session session) {
        this.session = session;
        final TableColumn<ByNameAndDateFilter, String> nameColumn = new TableColumn("Nom");
        nameColumn.setCellValueFactory(input -> new SimpleStringProperty(input.getValue().getName()));

        final TableColumn<ByNameAndDateFilter, LocalDate> dateColumn = new TableColumn("Date");
        dateColumn.setCellValueFactory(input -> (ObservableValue) new SimpleObjectProperty<>(input.getValue().getDate()));
        dateColumn.setCellFactory(DATE_CELL_FACTORY);

        // HACK for column name (we should not do it that way, but time...)
        String nbTitle = "Nombre";
        if (session.getDataContext() != null && session.getDataContext().getProtocol() != null) {
            final Protocol p = session.getDataContext().getProtocol();
            if (p instanceof P01)
                nbTitle = "Nombre d'horizons";
            else
                nbTitle = "Nombre d'observations";
        }
        nbColumn = new TableColumn(nbTitle);
        nbColumn.setCellValueFactory(this::getSubsetSize);

        getColumns().addAll(nameColumn, dateColumn, nbColumn);
        setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

        setItems(FXCollections.observableArrayList());

        dataset = session.getDataset().getItems().filtered(null); // Prepare a filtered list to allow user setting filters.
        dataset.addListener(this::datasetChanged);
        addFilters(dataset);
    }

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

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

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

    private ObservableValue<Integer> getSubsetSize(final TableColumn.CellDataFeatures<ByNameAndDateFilter, Integer> cellFeature) {
        // If given value and current dataset are not null, we set column value as tracking point subset size.
        if (cellFeature.getValue() != null && session != null && session.getDataset() != null) {
            return (ObservableValue) Bindings.size(dataset.filtered(cellFeature.getValue()));
        } else {
            return new SimpleObjectProperty<>(null);
        }
    }

    /**
     * Every time session dataset changes, we update list of available groups.
     * @param c Changes which occurred on surveyed data.
     */
    private void datasetChanged(final ListChangeListener.Change<? extends Statement> c) {
        while (c.next()) {
            if (c.wasUpdated()) {
                removeObsolete();
                addFilters(c.getList().subList(c.getFrom(), c.getTo()));
            } else {
                if (c.wasRemoved()) {
                    removeObsolete();
                }

                addFilters(c.getAddedSubList());
            }
        }
    }

    /**
     * Delete from table all useless entries. I.e all entries which don't have
     * any statement associated anymore, and not being currently edited.
     */
    private void removeObsolete() {
        /* HACK : To find obsolete groups, we just browse count column, and
         * delete matching items for each count equal to 0.
         */
        final Iterator<ByNameAndDateFilter> it = getItems().iterator();
        Integer nb;
        ByNameAndDateFilter current;
        while (it.hasNext()) {
            current = it.next();
            // Even if the current group is empty, we don't delete
            // it if user is working with it.
            if (current.equals(getSelectionModel().getSelectedItem()))
                continue;
            nb = nbColumn.getCellData(current);
            if (nb == null || nb == 0)
                it.remove();
        }
    }

    /**
     * Add filters matching given data name/date pairs.
     * @param dataset Data which will serve to build list of filters to add.
     */
    private void addFilters(final List<? extends Statement> dataset) {
        if (dataset == null || dataset.isEmpty())
            return;

        final HashSet<ByNameAndDateFilter> set = new HashSet<>();
        for (final Statement st : dataset) {
                set.add(new ByNameAndDateFilter(st.getTrackingPoint(), st.getDate()));
        }

        synchronized (this) {
            set.removeAll(getItems()); // Ensure no doublon is added.
            getItems().addAll(set);
        }
    }

    private static TableCell<ByNameAndDateFilter, LocalDate> createDateCell(final TableColumn<ByNameAndDateFilter, LocalDate> column) {
        return new TableCell<ByNameAndDateFilter, LocalDate>() {
            @Override
            protected void updateItem(LocalDate item, boolean empty) {
                super.updateItem(item, empty);
                if (empty)
                    setText(null);
                else if (item == null)
                    setText(" - ");
                else
                    setText(item.toString());
            }
        };
    }
}
