/**
 * 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.Rhomeo;
import fr.cenra.rhomeo.api.data.DataContext;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.ui.StatementEditor;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.protocol.p02.P02;
import fr.cenra.rhomeo.protocol.p05.P05;
import fr.cenra.rhomeo.protocol.p06.P06;
import fr.cenra.rhomeo.protocol.p07.P07;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javax.annotation.PostConstruct;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 *
 * This pane is a factorisation of common attributes and functionalities of
 * {@link P02}, {@link P05}, {@link P06} and {@link P07} {@link StatementEditor}s.
 *
 * It displays a summary of a "current" {@link TrackingPoint} one the one hand,
 * and a list of {@link T} on the other hand.
 *
 * The pane inherits Borderpane and must contain at least :
 *
 * 1- {@link TrackingPoint} attribute summary :
 *
 * - a {@link TextField} ({@link AbstractCustomStatementEditor#uiName}) mapping
 * the name of the current {@link TrackingPoint}.
 * - a {@link DatePicker} field ({@link AbstractCustomStatementEditor#uiDate}),
 * mapping the date of the current selected statement.
 * - a {@link TextArea} field ({@link AbstractCustomStatementEditor#uiRemarks}),
 * mapping the remarks of the current selected statement.
 *
 * 2- A {@link StatementTable} of the {@link T}s of the selected {@link TrackingPoint}.
 *
 * /!\ Inherited classes should be {@link Component}s, as a post-construct phase
 * and an autowired field are defined here.
 *
 * @author Samuel Andrés (Geomatys) <samuel.andres at geomatys.com>
 * @param <T>
 */
public abstract class AbstractCustomStatementEditor<T extends Statement> extends BorderPane implements StatementEditor<T> {

    @Autowired
    protected Session session;

    // Tracking point attributes
    @FXML protected TextField uiName;
    @FXML protected DatePicker uiDate;
    @FXML protected TextArea uiRemarks;

    // Statement control
    @FXML protected BorderPane uiObsContainer;
    @FXML protected Button uiDelete;
    protected StatementTable<T> uiStatementTable;

    protected final Class<T> dataType;

    /**
     * An internal field we use to know if the typed name has changed when user
     * exit the name field.
     */
    private String oldName = null;
    /**
     * An internal field we use to know if the typed date has changed when user
     * exit the date field.
     */
    private LocalDate oldDate = null;
    /**
     * An internal field we use to know if the typed remarks have changed when
     * user exit the remarks field.
     */
    private String oldRemarks = null;

    private final StringProperty firstEditionColumn = new SimpleStringProperty();

    protected AbstractCustomStatementEditor(final Class<T> dataType) {
        super();
        ArgumentChecks.ensureNonNull("Data type", dataType);
        this.dataType = dataType;
    }

    /**
     * Default {@link PostConstruct} AbstractCustomStatementEditor.
     */
    @PostConstruct
    protected void init() {
        try {
            final Class<? extends Statement> dataType = session.getDataContext().getProtocol().getDataType();
            if (getDataType().isAssignableFrom(dataType)) {
                // Default behaviour : ignore some statement attributes.
                uiStatementTable = new StatementTable(dataType, getFieldsToIgnore());
                uiStatementTable.setMinHeight(30);
                uiStatementTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
                final InvalidationListener listener = (Observable obs) -> {
                    for (final TableColumn col : uiStatementTable.getColumns()) {
                        if (col.isEditable() && (col instanceof PropertyColumn)) {
                            firstEditionColumn.set(((PropertyColumn)col).descriptor.getName());
                            break;
                        }
                    }
                };
                uiStatementTable.getColumns().addListener(listener);
                listener.invalidated(uiStatementTable.getColumns());

                uiObsContainer.setCenter((Node) uiStatementTable);

                uiName.focusedProperty().addListener(this::nameFocusChanged);
                uiDate.focusedProperty().addListener(this::dateFocusChanged);
                uiRemarks.focusedProperty().addListener(this::remarksFocusChanged);

                uiStatementTable.itemsProperty().addListener(this::itemsChanged);
                itemsChanged(uiStatementTable.itemsProperty(), null, null);
            }
        } catch (Exception e) {
            RhomeoCore.LOGGER.log(Level.WARNING, "Cannot initialize statement table !", e);
            final Label label = new Label("Impossible d'afficher la table des relevés.");
            label.getStyleClass().add(Rhomeo.CSS_ERROR_FIELD);
            uiObsContainer.setCenter(label);
        }
    }

    @Override
    public ObservableList<T> getItems() {
        return uiStatementTable.getItems();
    }

    @Override
    public void setItems(ObservableList<T> items) {
        uiStatementTable.setItems(items);
    }

    @Override
    public ObjectProperty<ObservableList<T>> itemsProperty() {
        return uiStatementTable.itemsProperty();
    }

    /**
     *
     * @return A list of properties from {@link #dataType} which must not be
     * included into edition table.
     */
    protected String[] getFieldsToIgnore() {
        return new String[]{"trackingPoint", "date", "remarks"};
    }

    /**
     * Default behaviour dataType return behaviour.
     *
     * Returns the data type of the {@link Protocol} of the current {@link Session}
     * {@link DataContext}.
     *
     * @return
     */
    @Override
    public Class<T> getDataType() {
        return dataType;
    }

    /**
     * Default behaviour to create a new {@link T}.
     *
     * Creates a new statement initializing common attributes from {@link TrackingPoint}.
     *
     * @return
     * @throws ReflectiveOperationException
     */
    protected T initStatement() throws ReflectiveOperationException {
        // TODO : try to acquire a prototype ?
        final T data = (T) session.getDataContext().getProtocol().getDataType().newInstance();

        if (uiName.getText() != null && !uiName.getText().trim().isEmpty()) {
            data.setTrackingPoint(uiName.getText());
        }

        if (uiDate.getValue() != null) {
            data.setDate(uiDate.getValue());
        }

        data.setRemarks(uiRemarks.getText());

        return data;
    }

    /**
     * Default behaviour on adding {@link T} event.
     *
     * Creates a new statement and focus on it.
     *
     * @param evt
     * @throws ReflectiveOperationException
     */
    @FXML
    protected void addStatement(final ActionEvent evt) throws ReflectiveOperationException {
        if (uiStatementTable == null || uiStatementTable.getItems() == null)
            return;

        final T data = initStatement();

        uiStatementTable.getItems().add(data);
        final TablePosition pos = new TablePosition(uiStatementTable, uiStatementTable.getItems().size() - 1, null);

        // HACK : It appears that table cells are refreshed on next pulse, so we wait for it before asking for focus.
        TaskManager.INSTANCE.submit(() -> {
            synchronized (pos) {
                try {
                    pos.wait(100);
                } catch (InterruptedException ex) {
                    Rhomeo.LOGGER.log(Level.WARNING, null, ex);
                }
            }

            Platform.runLater(() -> Rhomeo.editNextCell(pos));
        });
    }

    /**
     * Default behaviour of deleting {@link T} event.
     *
     * @param evt
     */
    @FXML
    protected void deleteStatements(final ActionEvent evt) {
        if (uiStatementTable == null || uiStatementTable.getItems() == null)
            return;

        // Defensive copy
        final List<Integer> indices = new ArrayList<>(uiStatementTable.getSelectionModel().getSelectedIndices());
        for (final int index : indices) {
            uiStatementTable.getItems().remove(index);
        }
    }

    @Override
    public void focusOn(T item, String propertyName) {
        uiStatementTable.focusOn(item, propertyName);
    }

    /**
     * Default behaviour on changing current {@link TrackingPoint}.
     *
     * @param obs
     * @param oldItems
     * @param newItems
     */
    protected void itemsChanged(final ObservableValue<? extends List> obs, final List<T> oldItems, final List<T> newItems) {
        uiDelete.disableProperty().unbind();
        if (newItems != null && !newItems.isEmpty()) {
            final T first = newItems.get(0);
            uiName.setText(first.getTrackingPoint());
            uiDate.setValue(first.getDate());
            uiRemarks.setText(first.getRemarks());

            if (uiStatementTable != null) {
                uiStatementTable.getSelectionModel().selectFirst();
            }

        } else {
            uiName.setText(null);
            uiDate.setValue(null);
            uiRemarks.clear();
        }

        if (uiStatementTable != null) {
            uiDelete.disableProperty().bind(uiStatementTable.getSelectionModel().selectedItemProperty().isNull());
        }

        uiName.requestFocus();
    }

    /**
     * Default behaviour when the focus has changed on {@link AbstractCustomStatementEditor#uiName}.
     *
     * @param obs
     * @param oldValue
     * @param newValue
     */
    protected void nameFocusChanged(final ObservableValue<? extends Boolean> obs, final Boolean oldValue, final Boolean newValue) {
        final String name = uiName.getText();
        if (Boolean.TRUE.equals(newValue)) {
            oldName = name;
        } else if (Boolean.FALSE.equals(newValue) && !Rhomeo.equivalent(oldName, name)) {
            // Update statements. Use defensive copy because each change of name can update list of available items.
            final Statement[] toUpdate = uiStatementTable.getItems().toArray(new Statement[uiStatementTable.getItems().size()]);
            for (int i = 0 ; i < toUpdate.length ; i++) {
                toUpdate[i].setTrackingPoint(name);
            }
        }
    }

    /**
     * Default behaviour when the focus has changed on {@link AbstractCustomStatementEditor#uiDate}.
     *
     * @param obs
     * @param oldValue
     * @param newValue
     */
    protected void dateFocusChanged(final ObservableValue<? extends Boolean> obs, final Boolean oldValue, final Boolean newValue) {
        final LocalDate date = uiDate.getValue();
        if (Boolean.TRUE.equals(newValue)) {
            oldDate = date;
        } else if (!(oldDate == date || oldDate != null && date != null && oldDate.isEqual(date))) {
            // Update statements. Use defensive copy because each change of name can update list of available items.
            final Statement[] toUpdate = uiStatementTable.getItems().toArray(new Statement[uiStatementTable.getItems().size()]);
            for (int i = 0 ; i < toUpdate.length ; i++) {
                toUpdate[i].setDate(date);
            }
        }
    }

    /**
     * Default behaviour when the focus has changed on {@link AbstractCustomStatementEditor#uiName}.
     *
     * @param obs
     * @param oldValue
     * @param newValue
     */
    protected void remarksFocusChanged(final ObservableValue<? extends Boolean> obs, final Boolean oldValue, final Boolean newValue) {
        final String remarks = uiRemarks.getText();
        if (Boolean.TRUE.equals(newValue)) {
            oldRemarks = remarks;
        } else if (Boolean.FALSE.equals(newValue) && !Rhomeo.equivalent(oldRemarks, remarks)) {
            for (final Statement st : uiStatementTable.getItems()) {
                st.setRemarks(remarks);
            }
        }
    }
}
