/**
 * 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.DataContext;
import fr.cenra.rhomeo.api.data.Protocol;
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.ui.ProtocolEditorSpi;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.core.data.DataContextManager;
import fr.cenra.rhomeo.core.data.ReferenceManager;
import fr.cenra.rhomeo.fx.edition.FXDatasetPane;

import java.beans.IntrospectionException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;

import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.StageStyle;
import javax.annotation.PostConstruct;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Menu banner for selected protocol page.
 *
 * @author Cédric Briançon (Geomatys)
 */
@Component
public class FXRhomeoWizard extends VBox {

    @FXML private Label uiSelectedSiteLbl;
    @FXML private Label uiSelectedProtocolLbl;

    @FXML private VBox uiHeader;
    @FXML private BorderPane uiBody;

    @FXML private HBox uiProtocolChosenBox;
    @FXML private GridPane uiProtocolStepsGrid;

    @FXML private Button uiRefUsedBtn;
    @FXML private Button uiStopProcessBtn;

    @FXML private Button uiStep1Btn;
    @FXML private Button uiStep2Btn;
    @FXML private Button uiStep3Btn;
    @FXML private Button uiStep4Btn;

    @Autowired
    private DataContextManager contextManager;

    @Autowired
    private Session session;

    @Autowired
    List<ProtocolEditorSpi> editorSpis;

    private DataContext context;
    private Site site;
    private Protocol protocol;
    private ProtocolEditorSpi editorSpi;

    private final Map<WorkflowStep, Button> stepBtns;

    private final String stopBtnOriginalText;

    public FXRhomeoWizard() {
        Rhomeo.loadFXML(this, FXRhomeoWizard.class);
        // Index buttons by step
        final Map<WorkflowStep, Button> tmpMap = new HashMap<>();
        tmpMap.put(WorkflowStep.DATASET, uiStep1Btn);
        tmpMap.put(WorkflowStep.PROCESS, uiStep2Btn);
        tmpMap.put(WorkflowStep.RESULT, uiStep3Btn);
        tmpMap.put(WorkflowStep.FINALIZATION, uiStep4Btn);
        stepBtns = Collections.unmodifiableMap(tmpMap);
        for (final Map.Entry<WorkflowStep, Button> entry : stepBtns.entrySet()) {
            entry.getValue().setOnAction(evt -> session.requestWorkflowStep(entry.getKey()));
        }

        uiHeader.managedProperty().bind(uiHeader.visibleProperty());

        final ImageView im = new ImageView(Rhomeo.ICON_EXCHANGE_GRAY);
        im.setRotate(90);
        uiRefUsedBtn.setGraphic(im);
        uiStopProcessBtn.setGraphic(new ImageView(Rhomeo.ICON_STOP_RED));
        // HACK #77 : User want a change on the button which stops process on a
        // specific step, so we keep reference to its original text now.
        stopBtnOriginalText = uiStopProcessBtn.getText();

        // Prepare display of arrows linking step buttons. Each arrow state is bound
        // to the step button following it.
        final ImageView imgArrow1 = new ImageView(Rhomeo.ICON_LONG_ARROW_RIGHT);
        imgArrow1.setDisable(true);
        imgArrow1.visibleProperty().bind(uiStep2Btn.visibleProperty());
        imgArrow1.managedProperty().bind(imgArrow1.visibleProperty());
        final ImageView imgArrow2 = new ImageView(Rhomeo.ICON_LONG_ARROW_RIGHT);
        imgArrow2.setDisable(true);
        imgArrow2.visibleProperty().bind(uiStep3Btn.visibleProperty());
        imgArrow2.managedProperty().bind(imgArrow2.visibleProperty());
        final ImageView imgArrow3 = new ImageView(Rhomeo.ICON_LONG_ARROW_RIGHT);
        imgArrow3.setDisable(true);
        imgArrow3.visibleProperty().bind(uiStep4Btn.visibleProperty());
        imgArrow3.managedProperty().bind(imgArrow3.visibleProperty());
        // Limit height of arrow image, which is really too big (lot of empty space).
        final Rectangle2D arrowDisplay = new Rectangle2D(
                0, Rhomeo.ICON_LONG_ARROW_RIGHT.getHeight() / 4,
                Rhomeo.ICON_LONG_ARROW_RIGHT.getWidth(), Rhomeo.ICON_LONG_ARROW_RIGHT.getHeight() / 2);
        imgArrow1.setViewport(arrowDisplay);
        imgArrow2.setViewport(arrowDisplay);
        imgArrow3.setViewport(arrowDisplay);

        uiProtocolStepsGrid.add(imgArrow1, 2, 0);
        uiProtocolStepsGrid.add(imgArrow2, 4, 0);
        uiProtocolStepsGrid.add(imgArrow3, 6, 0);
    }

    @PostConstruct
    private void init() {
        session.workflowStepProperty().addListener(this::stepChanged);
        unloadDataContext();
    }

    private void loadDataContext() {
        this.context = session.getDataContext();
        this.site = context.getSite();
        this.protocol = context.getProtocol();
        initializeReferences();
        for (final ProtocolEditorSpi spi : editorSpis) {
            if (spi.getProtocol() == protocol) {
                editorSpi =spi;
                break;
            }
        }

        // Deactivate step buttons for workflow parts marked as non available
        // by the input SPI.
        if (editorSpi != null && !editorSpi.getStepsToIgnore().isEmpty()) {
            for (final WorkflowStep step : editorSpi.getStepsToIgnore()) {
                final Button btn = stepBtns.get(step);
                if (btn != null) {
                    btn.setVisible(false);
                    btn.setManaged(false);
                }
            }
        }

        uiSelectedSiteLbl.setText(site.getName());
        uiSelectedProtocolLbl.setText(protocol.getName());
        uiProtocolChosenBox.getChildren().add(uiSelectedProtocolLbl);
        uiRefUsedBtn.setDisable(protocol.getReferenceTypes().isEmpty());

        final List<Indicator> indicators = session.getIndicators();
        indicators.stream()
                .filter(indicator -> indicator.getProtocol() == protocol)
                .sorted()
                .forEach(indicator -> {
                    final Label lbl = new Label(indicator.getName());
                    lbl.getStyleClass().add(Rhomeo.CSS_INDICATOR_LABEL);
                    uiProtocolChosenBox.getChildren().add(lbl);
                });

        final Button pdfBtn = Rhomeo.createProtocolDocumentButton(protocol);
        pdfBtn.setText(null);
        pdfBtn.setPrefHeight(28);
        pdfBtn.setMaxHeight(USE_PREF_SIZE);
        pdfBtn.setMinHeight(USE_PREF_SIZE);
        uiProtocolChosenBox.getChildren().add(pdfBtn);
        uiHeader.setVisible(true);

        // Generate protocol background
        final String bgPath = "/fr/cenra/rhomeo/images/" + protocol.getName().toLowerCase() + ".png";
        if (FXRhomeoWizard.class.getResource(bgPath) != null) {
            try (final InputStream stream = FXRhomeoWizard.class.getResourceAsStream(bgPath)) {
                final Background backgroundProtocol = new Background(new BackgroundImage(
                        new Image(stream),
                        BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT,
                        new BackgroundPosition(Side.LEFT, 0, true, Side.BOTTOM, 0, true), BackgroundSize.DEFAULT));
                setBackground(backgroundProtocol);
            } catch (IOException e) {
                Rhomeo.LOGGER.log(Level.FINE, "Cannot load a background image !", e);
            }
        }

        // #59 Reset results if user change reference version.
        session.getDataContext().getReferences().addListener((Observable obs) -> {
            if (session.getWorkflowStep() == WorkflowStep.RESULT || session.getWorkflowStep() == WorkflowStep.FINALIZATION)
                session.requestWorkflowStep(WorkflowStep.PROCESS);

            session.setResults(null);
            stepBtns.get(WorkflowStep.RESULT).setDisable(true);
            stepBtns.get(WorkflowStep.FINALIZATION).setDisable(true);
        });
    }

    private void unloadDataContext() {
        final Site tmpSite = site;
        context = null;
        site = null;
        protocol = null;
        uiProtocolChosenBox.getChildren().clear();
        uiHeader.setVisible(false);
        editorSpi = null;

        for (final Button stepBtn : stepBtns.values()) {
            stepBtn.setDisable(true);
            stepBtn.setVisible(true);
            stepBtn.getStyleClass().removeAll(Rhomeo.CSS_CURRENT_STEP);
        }

        final FXDashboardPane dashboard = session.getBean(FXDashboardPane.class);
        dashboard.siteProperty().set(tmpSite);

        // TODO : set site
        uiBody.setCenter(dashboard);
        setBackground(Background.EMPTY);
    }

    /**
     * On {@link FXRhomeoWizard#uiRefUsedBtn} action.
     */
    @FXML
    private void showUsedRef() {
        final DataContext dc = session.getDataContext();
        if (dc == null) {
            return;
        }
        final Protocol protocol = dc.getProtocol();
        if (protocol == null) {
            return;
        }

        final Stage stage = Rhomeo.newStage();
        stage.setTitle("Choix des référentiels");
        final VBox vbox = new VBox(10);
        vbox.getStyleClass().add("white-box");

        protocol.getReferenceTypes().forEach(description -> {
            try {
                final FXProtocolReferencesPane protocolReferences = new FXProtocolReferencesPane(description);
                vbox.getChildren().add(protocolReferences);
                VBox.setVgrow(protocolReferences, Priority.SOMETIMES);
            } catch (IntrospectionException ex) {
                throw new RhomeoRuntimeException(ex);
            }
        });

        final Button validBtn = new Button("Valider");
        validBtn.getStyleClass().add("white-button");
        validBtn.setOnAction(event -> {
            // #59 If user already computed indices, changing reference versions
            // would delete his results, so we warn him.
            if (!stepBtns.get(WorkflowStep.RESULT).isDisabled()) {
                Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Si vous changez de version de référentiel maintenant, vos résultats seront supprimés. Continuer ?", ButtonType.NO, ButtonType.YES);
                alert.setResizable(true);
                if (ButtonType.NO.equals(alert.showAndWait().orElse(ButtonType.NO)))
                    return;
            }
            vbox.getChildren().stream()
                    .filter(child -> child instanceof FXProtocolReferencesPane)
                    .map(child -> (FXProtocolReferencesPane)child)
                    .forEach(protoRefs ->
                        protoRefs.getItems().stream()
                                .filter(o -> o instanceof FXProtocolReferencesPane.ReferenceLineItem && ((FXProtocolReferencesPane.ReferenceLineItem) o).isSelected())
                                .forEach(o -> dc.getReferences().put(protoRefs.getReferenceClass(), ((FXProtocolReferencesPane.ReferenceLineItem) o).getVersion()))
                    );
            try {
                contextManager.writeDataContext(dc);
            } catch (IOException ex) {
                throw new RhomeoRuntimeException(ex);
            } finally {
                stage.close();
            }
        });

        final Button cancelBtn = new Button("Annuler");
        cancelBtn.setCancelButton(true);
        cancelBtn.setOnAction(evt -> stage.close());
        cancelBtn.getStyleClass().add("white-button");

        final HBox validHbox = new HBox(5, cancelBtn, validBtn);
        validHbox.setAlignment(Pos.CENTER_RIGHT);
        vbox.getChildren().add(validHbox);

        final Scene scene = new Scene(vbox);
        scene.getStylesheets().add(Rhomeo.CSS_THEME_PATH);
        scene.getStylesheets().add("/fr/cenra/rhomeo/fx/FXProtocolReferencesPane.css");
        stage.setScene(scene);
        stage.setMinHeight(protocol.getReferenceTypes().size() * 130 + 80);
        stage.setHeight(stage.getMinHeight());
        stage.setResizable(true);
        stage.initModality(Modality.APPLICATION_MODAL);
        stage.initStyle(StageStyle.UNDECORATED);
        stage.sizeToScene();
        stage.showAndWait();
    }

    @FXML
    private void stopProcess() {
        // HACK #78 : Before stopping session, we check dataset state, for the
        // user to be able to save its data before stopping.
        final Node center = uiBody.getCenter();
        if (center instanceof FXDatasetPane) {
            Task<Boolean> saveTask = ((FXDatasetPane) center).checkSaveChange().orElse(null);
            if (saveTask != null)
                saveTask.setOnSucceeded(evt -> {
                    if (Boolean.TRUE.equals(evt.getSource().getValue()))
                        Platform.runLater(() -> session.requestWorkflowStep(null));
                });
            else
                session.requestWorkflowStep(null);
        } else
            session.requestWorkflowStep(null);
    }

    private void stepChanged(final ObservableValue<? extends WorkflowStep> obs, final WorkflowStep oldStep, final WorkflowStep newStep) {
        // HACK #77 : User want a change on stop button look and feel at final step.
        // We reset its state before doing anything else.
        uiStopProcessBtn.setText(stopBtnOriginalText);
        uiStopProcessBtn.setGraphic(new ImageView(Rhomeo.ICON_STOP_RED));

        if (newStep == null) {
            // Clear and hide edition display. Go back to the dashboard.
            unloadDataContext();

        } else {

            if (oldStep == null) {
                // New edition started
                loadDataContext();
            } else {
                Button btn = stepBtns.get(oldStep);
                if (btn != null && btn.getStyleClass().contains(Rhomeo.CSS_CURRENT_STEP)) {
                    btn.getStyleClass().remove(Rhomeo.CSS_CURRENT_STEP);
                }
            }

            // Init current step UI
            Button btn = stepBtns.get(newStep);
            btn.setDisable(false);
            btn.getStyleClass().add(Rhomeo.CSS_CURRENT_STEP);

            Node body = createNode(newStep);
            if (body == null) {
                switch (newStep) {
                    case DATASET:
                        body = session.getBean(FXDatasetPane.class);
                        break;

                    case PROCESS:
                        body = session.getBean(FXDefaultProcessingPane.class);
                        break;

                    case RESULT:
                        body = session.getBean(FXDefaultResultPane.class);
                        break;

                    case FINALIZATION:
                        body = session.getBean(FXFinalizationPane.class);
                        // HACK #77 : Change look and feel of stop button.
                        uiStopProcessBtn.setText("Retour au tableau de bord");
                        uiStopProcessBtn.setGraphic(new ImageView(Rhomeo.ICON));
                        break;
                }
            }

            uiBody.setCenter(body);
        }
    }

    private Node createNode(final WorkflowStep step) {
        final Node node;
        if (editorSpi != null) {
            node = editorSpi.getEditor(step);
        } else {
            node = null;
        }

        return node;
    }

    /**
     * Initialize references lists for further use.
     * Note : For each reference of the current protocol, we update list of
     * installed and available versions. If none is installed, we propose to the
     * user to install the most up to date.
     *
     * @throws IntrospectionException
     */
    private void initializeReferences() {
        if (protocol == null || protocol.getReferenceTypes().isEmpty())
            return;

        for (final ReferenceDescription description : protocol.getReferenceTypes()) {
            try {
                final ReferenceManager referenceManager = ReferenceManager.getOrCreate(description);
                final Task<ReferenceManager> task = referenceManager.refresh();
                task.setOnSucceeded(event -> {
                    // If we haven't any version installed for this reference, then we propose to install the newest.
                    if (referenceManager.getInstalledVersions().isEmpty() && !referenceManager.getDistantVersions().isEmpty()) {
                        Platform.runLater(() -> {
                            final StringBuilder builder = new StringBuilder("Aucune version du référentiel suivant n'est installée :")
                                    .append(System.lineSeparator())
                                    .append(description.getTitle())
                                    .append(System.lineSeparator())
                                    .append(System.lineSeparator())
                                    .append("Voulez-vous télécharger la plus récente ?");
                            final Alert alert = new Alert(Alert.AlertType.CONFIRMATION, builder.toString(), ButtonType.NO, ButtonType.YES);
                            alert.setHeaderText("Référentiel non installé");
                            alert.setResizable(true);
                            // If agreed, we download newest version. When it's done, we set it as used version.
                            if (ButtonType.YES.equals(alert.showAndWait().orElse(ButtonType.NO))) {
                                final Version max = Collections.max((Set<Version>) referenceManager.getDistantVersions());
                                TaskManager.INSTANCE.submit(referenceManager.install(max)).setOnSucceeded(evt -> {
                                    final DataContext dc = session.getDataContext();
                                    if (dc != null)
                                        Platform.runLater(() -> dc.getReferences().put(description.getReferenceType(), max));
                                });
                            }
                        });
                    } else {
                        final Version selectedVersion = context.getReferences().get(description.getReferenceType());
                        // If at least one local version exists for this reference and nothing has been selected, then select the biggest one.
                        // This way, for each reference a version should be selected.
                        if (selectedVersion == null && !referenceManager.getInstalledVersions().isEmpty()) {
                            final Version max = Collections.max((Set<Version>) referenceManager.getInstalledVersions());
                            context.getReferences().put(description.getReferenceType(), max);
                        }
                    }
                });

                task.setOnFailed(event -> {
                    RhomeoCore.LOGGER.log(Level.WARNING, "Cannot update reference versions !", event.getSource().getException());
                    final Version selectedVersion = context.getReferences().get(description.getReferenceType());
                    // If at least one local version exists for this reference and nothing has been selected, then select the biggest one.
                    // This way, for each reference a version should be selected.
                    if (selectedVersion == null && !referenceManager.getInstalledVersions().isEmpty()) {
                        final Version max = Collections.max((Set<Version>) referenceManager.getInstalledVersions());
                        context.getReferences().put(description.getReferenceType(), max);
                    }
                });

                TaskManager.INSTANCE.submit(task);
            } catch (Exception e) {
                Rhomeo.LOGGER.log(Level.WARNING, "Cannot initialize reference !", e);
            }
        }
    }
}
