/**
 * 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.InternationalDescription;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.data.Warning;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.AbstractParametrableIndex;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.api.result.IndexMapper;
import fr.cenra.rhomeo.api.result.IndexSpi;
import fr.cenra.rhomeo.api.ui.AdditionalResultSpi;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.core.result.ResultWriter;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.FileSystem;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.logging.Level;
import javafx.application.Platform;

import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
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;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;


/**
 * The result pane controller.
 *
 * @author Samuel Andrés (Geomatys)
 *
 */
@Component("fr.cenra.rhomeo.fx.FXDefaultResultPane")
@Scope("prototype")
public class FXDefaultResultPane extends VBox implements InternationalResource {

    /**
     * Key (or key-part) that must be used to retrieve specific headers from
     * {@link IndexSpi} and or base classes implementing {@link InternationalResource},
     * to complementary table headers.
     */
    public static final String COMPLEMENTARY_TABLE_HEADER_PROPERTY_KEY = "result-pane.complementary-table";

    /**
     * Fields taken into account to build columns mapping base classes into
     * complementary tables.
     */
    private static final List<String> BASE_FIELDS = Arrays.asList("name", "year");

    @FXML private Label uiSiteLbl;
    @FXML private TableView<IndexMapper<Integer>> uiSitePane;
    @FXML private GridPane uiInvalidateGridPane;
    @FXML private TableColumn<IndexMapper<Integer>, Integer> uiSiteYearColumn;
    @FXML private ToggleButton uiDisplayValuesToggleBtn;
    @FXML private ScrollPane uiAdditionalValuesScrollPane;
    @FXML private VBox uiAdditionalValuesBox;
    @FXML private VBox uiTableBox;

    @FXML private Label uiYearsLbl;
    @FXML private Label uiIndicatorLbl;
    @FXML private Label uiDataStatusLbl;
    @FXML private Label uiAdditionalLabel;

    @Autowired
    private Session session;

    @Autowired
    private Validator validator;

    @Autowired
    private ResultWriter resultWriter;

    @Autowired
    private List<AdditionalResultSpi> additionalSpis;

    private final SimpleBooleanProperty isInvalid = new SimpleBooleanProperty(false);

    /**
     *
     */
    public FXDefaultResultPane() {
        Rhomeo.loadFXML(this, FXDefaultResultPane.class);
        uiSitePane.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        uiSitePane.setFixedCellSize(25);
        uiAdditionalLabel.visibleProperty().bind(Bindings.isNotEmpty(uiAdditionalValuesBox.getChildren()));
    }

    /**
     * Initializes JavaFX components after spring components are done loading.
     */
    @PostConstruct
    public void postConstruct() {
        final Set<Index> results = session.getResults();
        ArgumentChecks.ensureNonNull("results", results);

        /*
        1- Schedule.
        ==========*/

        // Determine indicators and schedule results.

        final Map<Integer, IndexMapper<Integer>> mainResults = new HashMap<>();
        final ArrayList<IndexSpi> mainSpis = new ArrayList<>();
        final List<Indicator> indicators = new ArrayList<>();
        final Map<Class, Map<Object, IndexMapper>> complementaryResults = new HashMap<>();
        final Map<Integer, IndexMapper<Integer>> byYearComplementary = new HashMap<>();
        final Map<Class, List<IndexSpi>> indexSpis = new HashMap<>();
        for(final Index index : results){

            // Seach for index indicators.
            final Indicator indicator = index.getSpi().getIndicator();
            if(!indicators.contains(indicator)){
                indicators.add(indicator);
            }

            // Detect main indices for each indicator.
            if (index.getSpi().isPrimary()) {
                final int year = index.getYear();
                final IndexMapper<Integer> mapper = mainResults.computeIfAbsent(year, (input) -> new IndexMapper<>(input));
                if(!mainSpis.contains(index.getSpi()))
                    mainSpis.add(index.getSpi());
                mapper.setValue(index.getSpi().getName(), index.getValue());
            }
            // Schedule AbstractParametrableIndexes by base type.
            else if(index instanceof AbstractParametrableIndex) {
                final AbstractParametrableIndex parametrableIndex = (AbstractParametrableIndex) index;
                final Class baseClass = parametrableIndex.getBaseClass();
                final Object base = parametrableIndex.getBase();
                final IndexSpi indexSpi = parametrableIndex.getSpi();

                if(complementaryResults.get(baseClass)==null){
                    complementaryResults.put(baseClass, new HashMap<>());
                }
                if(complementaryResults.get(baseClass).get(base)==null){
                    complementaryResults.get(baseClass).put(base, new IndexMapper<>(base));
                }
                final IndexMapper indexMapper = complementaryResults.get(baseClass).get(base);
                indexMapper.setValue(indexSpi.getName(), parametrableIndex.getValue());

                //
                if(indexSpis.get(baseClass)==null) indexSpis.put(baseClass, new ArrayList<>());
                final List<IndexSpi> spis = indexSpis.get(baseClass);
                if(!spis.contains(indexSpi)) spis.add(indexSpi);

            } else {
                final int year = index.getYear();
                final IndexMapper<Integer> mapper = byYearComplementary.computeIfAbsent(year, (input) -> new IndexMapper<>(input));
                final List<IndexSpi> spis = indexSpis.computeIfAbsent(Integer.class, i -> new ArrayList<>());
                if(!spis.contains(index.getSpi())) spis.add(index.getSpi());
                mapper.setValue(index.getSpi().getName(), index.getValue());
            }
        }

        // Sort indicators.
        Collections.sort(indicators);

        /*
        2- Main indicators.
        =================*/

        // Build header message listing indicators
        final StringBuilder sbIndicator = new StringBuilder();
        sbIndicator.append("Indicateur(s) : ");

        final Protocol currentProtocol = session.getDataContext().getProtocol();
        boolean firstIndic = true;
        for(final Indicator indicator : indicators){
            if (!firstIndic) {
                sbIndicator.append(", ");
            }

            sbIndicator.append(indicator.getName()).append(" - ").append(indicator.getRemarks());
            firstIndic = false;
        }
        sbIndicator.append(" - ").append(currentProtocol.getRemarks());

        // Create columns for main indicator pane.
        uiSiteYearColumn.setCellValueFactory(value -> value.getValue().keyProperty());
        mainSpis.stream().sorted().forEach(spi -> {
            // Sometimes the displayed column name seems to be from the indexSpi itself, sometimes from its indicator (i.e. a specific name)
            final TableColumn<IndexMapper<Integer>, Number> column = new TableColumn<>(getIndexSpiColumnName(spi));
            column.setCellFactory(Rhomeo.ROUNDING_CELL_FACTORY);
            column.setCellValueFactory(value -> value.getValue().valueProperty(spi.getName()));
            uiSitePane.getColumns().add(column);
        });
        uiSitePane.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

        // Feed main indicator pane.
        final ObservableList<IndexMapper<Integer>> items = FXCollections.observableArrayList(mainResults.values());
        uiSitePane.setItems(items);
        uiSitePane.prefHeightProperty().bind(Bindings.size(items).multiply(uiSitePane.fixedCellSizeProperty()).add(40));

        // Summary
        uiYearsLbl.setGraphic(new ImageView(Rhomeo.ICON_CIRCLE_GRAY));
        final StringBuilder sbYears = new StringBuilder();
        sbYears.append("Année(s) : ");
        boolean firstYear = true;
        final List<Integer> years = new ArrayList<>(mainResults.keySet());
        Collections.sort(years);
        for (final Integer year : years) {
            if (!firstYear) {
                sbYears.append(", ");
            }
            sbYears.append(year);
            firstYear = false;
        }
        uiYearsLbl.setText(sbYears.toString());

        uiIndicatorLbl.setGraphic(new ImageView(Rhomeo.ICON_CIRCLE_GRAY));
        uiIndicatorLbl.setText(sbIndicator.toString());

        isInvalid.addListener((obj) -> {
            if (!isInvalid.get()) {
                uiDataStatusLbl.setGraphic(new ImageView(Rhomeo.ICON_CHECK_GREEN));
                uiDataStatusLbl.setText("Les données sont conformes au protocole : "+ currentProtocol.getRemarks());
            } else {
                uiDataStatusLbl.setGraphic(new ImageView(Rhomeo.ICON_TIMES_RED));
                uiDataStatusLbl.setText("Les données ne sont pas conformes au protocole : "+ currentProtocol.getRemarks());
            }
        });
        isInvalid.set(true);

        uiInvalidateGridPane.visibleProperty().bind(isInvalid);
        uiInvalidateGridPane.managedProperty().bind(uiInvalidateGridPane.visibleProperty());

        uiTableBox.visibleProperty().bind(isInvalid.not().or(uiDisplayValuesToggleBtn.selectedProperty()));
        uiTableBox.managedProperty().bindBidirectional(uiSiteLbl.visibleProperty());

        // Verify validation.
        validate();

        /*
        3- Other indicators.
        ==================*/

        // A table for complementary results displayed by year.
        if (!byYearComplementary.isEmpty()) {
            final TableView<IndexMapper<Integer>> table = new TableView<>(FXCollections.observableArrayList(byYearComplementary.values()));
            final TableColumn<IndexMapper<Integer>, Integer> yearColumn = new TableColumn("Année");
            yearColumn.setCellValueFactory(input -> input.getValue() == null ? null : input.getValue().keyProperty());
            table.getColumns().add(yearColumn);
            // index value columns
            indexSpis.get(Integer.class).stream().sorted().forEach(spi -> {
                // Sometimes the displayed column name seems to be from the indexSpi itself, sometimes from its indicator (i.e. a specific name)
                final TableColumn<IndexMapper<Integer>, Number> column = new TableColumn<>(getIndexSpiColumnName(spi));
                column.setCellFactory(Rhomeo.ROUNDING_CELL_FACTORY);
                column.setCellValueFactory(value -> value.getValue().valueProperty(spi.getName()));
                table.getColumns().add(column);
            });
            table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
            uiAdditionalValuesBox.getChildren().add(table);
        }

        // Create one TableView by base class.
        for(final Class baseClass : complementaryResults.keySet()){
            try {
                final TableView<IndexMapper> tableView = buildIndexMapperTableView(baseClass, indexSpis.get(baseClass));
                final ObservableList<IndexMapper> additionalItems = FXCollections.observableArrayList(complementaryResults.get(baseClass).values());
                tableView.setItems(additionalItems);
                tableView.setFixedCellSize(25);
                tableView.setMinHeight(70);
                tableView.setMaxHeight(300);
                tableView.prefHeightProperty().bind(Bindings.size(additionalItems).multiply(tableView.fixedCellSizeProperty()).add(40));
                uiAdditionalValuesBox.getChildren().add(tableView);
            } catch (IntrospectionException ex) {
                RhomeoCore.LOGGER.log(Level.WARNING, "Could not create tableview mapping {0}.", baseClass);
            }
        }

        final Button nextStepButton = new Button(getResourceString("proceed"));
        nextStepButton.getStyleClass().add("contrast");
        nextStepButton.setOnAction(value -> {
            session.requestWorkflowStep(WorkflowStep.FINALIZATION);
        });
        nextStepButton.visibleProperty().bind(isInvalid.not());
        nextStepButton.managedProperty().bind(nextStepButton.visibleProperty());
        final HBox bottomHBox = new HBox(nextStepButton);
        bottomHBox.setAlignment(Pos.CENTER_RIGHT);
        bottomHBox.setFillHeight(true);
        bottomHBox.setPrefSize(USE_PREF_SIZE, USE_PREF_SIZE);
        getChildren().add(bottomHBox);

        for (AdditionalResultSpi spi : additionalSpis) {
            spi.getResultNode(session.getAdditionalValues()).ifPresent(node -> uiAdditionalValuesBox.getChildren().add(node));
        }
    }

    /**
     * Verify dataset validation.
     */
    private void validate() {
        final Runnable taskInit = () -> TaskManager.INSTANCE.submit("Validation du lot de données", () -> {
            final Set<ConstraintViolation<Dataset>> violations = validator.validate(session.getProcessContext().getData());
            final Iterator<ConstraintViolation<Dataset>> it =  violations.iterator();
            while (it.hasNext()) {
                if (!it.next().getConstraintDescriptor().getPayload().contains(Warning.class)) {
                    return true;
                }
            }

            return false;

        }).setOnSucceeded(evt -> Platform.runLater(() -> isInvalid.set((boolean)evt.getSource().getValue())));
        if (Platform.isFxApplicationThread())
            taskInit.run();
        else Platform.runLater(taskInit);
    }

    @FXML
    public void exportData() {
        final String initialFileName = session.getDataContext().getSite().getName().concat(".zip");
        final FileChooser.ExtensionFilter zipFilter = new FileChooser.ExtensionFilter("Archive zip", "*.zip");
        final File outputFile = Rhomeo.askOutputFilePopup(initialFileName, this, zipFilter);
        if (outputFile == null) {
            return;
        }

        TaskManager.INSTANCE.submit("Export de résultats", () -> {
            try (final FileSystem fs = RhomeoCore.toZipFileSystem(outputFile.toPath())) {
                resultWriter.prepareWriting(fs.getRootDirectories().iterator().next()).writeMetadataText().writeResults().writeAdditionalValues();
            }
            return true;
        });
    }

    /**
     * Builds a complementary table for indexes associated to a given base class (for instance {@link TrackingPoint}).
     *
     * This method use introspection on the given class, but it only displays information from fields which name
     * are specified in {@link FXDefaultResultPane#BASE_FIELDS}.
     *
     * @param <T>
     * @param baseClass
     * @return
     *
     * @throws IntrospectionException
     */
    private <T> TableView<IndexMapper<T>> buildIndexMapperTableView(final Class<T> baseClass, final List<IndexSpi> indexSpis) throws IntrospectionException {

        final BeanInfo beanInfo = Introspector.getBeanInfo(baseClass);

        final TableView<IndexMapper<T>> tableView = new TableView<>();
        tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

        // Index base columns
        for (final PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) {

            final String dName = desc.getName();
            if(BASE_FIELDS.contains(dName)){

                final TableColumn<IndexMapper<T>, ?> col = new TableColumn<>(getBaseColumnName(baseClass, dName));
                col.setCellValueFactory(value -> {
                    try {
                        final Method reader = desc.getReadMethod();
                        if(reader!=null && value!=null && value.getValue()!=null && value.getValue() instanceof IndexMapper){
                            final T target = ((IndexMapper<T>) value.getValue()).getKey();
                            if(target!=null){
                                return new SimpleObjectProperty(reader.invoke(target));
                            }
                            else return null;
                        }
                        else return null;
                    } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException ex) {
                        RhomeoCore.LOGGER.log(Level.INFO, "Unable to build CellValueFactory for property {0}.", dName);
                        return null;
                    }
                });
                tableView.getColumns().add(col);
            }
        }

        // index value columns
        indexSpis.stream().sorted().forEach(spi -> {
            // Sometimes the displayed column name seems to be from the indexSpi itself, sometimes from its indicator (i.e. a specific name)
            final TableColumn<IndexMapper<T>, Number> column = new TableColumn<>(getIndexSpiColumnName(spi));
            column.setCellValueFactory(value -> value.getValue().valueProperty(spi.getName()));
            column.setCellFactory(Rhomeo.ROUNDING_CELL_FACTORY);
            tableView.getColumns().add(column);
        });

        return tableView;
    }

    /**
     * Builds a name for the column mapping a given base class field in a complementary table.
     *
     * @param baseClass
     * @param propertyName
     * @return
     */
    private static String getBaseColumnName(final Class baseClass, final String propertyName){

        // If indexSpi implements InternationalResource, try to use specific property
        if(InternationalResource.class.isAssignableFrom(baseClass)){

            // 1- Try to retrieve some resource specific to a complementary result table.
            try{
                return InternationalResource.getResourceString(baseClass, propertyName, COMPLEMENTARY_TABLE_HEADER_PROPERTY_KEY);
            }
            catch(MissingResourceException ex1){

                // 2- Try to retrive resource mapping the property name.
                try{
                    return InternationalResource.getResourceString(baseClass, propertyName);
                }
                catch(MissingResourceException ex2){
                    RhomeoCore.LOGGER.log(Level.FINEST, "Missing ressource to display column name from {0}", InternationalResource.class.getSimpleName());
                }
            }
        }

        // If no better can be done, return the property name.
        return propertyName;
    }

    /**
     * Builds a name for the column mapping a given {@link IndexSpi} in a complementary table.
     *
     * @param indexSpi
     * @return
     */
    private static String getIndexSpiColumnName(final IndexSpi indexSpi){

        // If indexSpi implements InternationalResource, try to use specific property
        if(indexSpi instanceof InternationalResource){
            try{
                return ((InternationalResource) indexSpi).getResourceString(COMPLEMENTARY_TABLE_HEADER_PROPERTY_KEY);
            }
            catch(MissingResourceException e){
                RhomeoCore.LOGGER.log(Level.FINEST, "Missing ressource to display column name from {0}", InternationalResource.class.getSimpleName());
            }
        }

        // If previous try failed and indexSpi implements InternationalDescription, use InternationalDescription label.
        if(indexSpi instanceof InternationalDescription) return ((InternationalDescription) indexSpi).getLabel();

        // If no other solution is found, return indexSpi name.
        return indexSpi.getTitle();
    }
}
