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

import com.sun.javafx.PlatformUtil;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.data.UpdateInfo;
import fr.cenra.rhomeo.core.preferences.net.NetPreferences;
import fr.cenra.rhomeo.core.state.StateManager;
import fr.cenra.rhomeo.fx.FXInitialDialogPane;
import fr.cenra.rhomeo.fx.FXMainPane;
import fr.cenra.rhomeo.fx.FXSplashScreen;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.DialogEvent;
import javafx.scene.control.TextArea;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.controlsfx.dialog.ExceptionDialog;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.geotoolkit.internal.GeotkFX;
import org.slf4j.bridge.SLF4JBridgeHandler;
import org.springframework.context.support.ClassPathXmlApplicationContext;


/**
 * Application launcher.
 *
 * @author Cédric Briançon (Geomatys)
 */
public class Launcher extends Application {
    /**
     * Launches the application with the given arguments.
     *
     * @see #launch(String...)
     */
    public static void main(String[] args) {
        launch(args);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        SLF4JBridgeHandler.removeHandlersForRootLogger();  // (since SLF4J 1.6.5)

        // add SLF4JBridgeHandler to j.u.l's root logger, should be done once during
        // the initialization phase of your application
        SLF4JBridgeHandler.install();

        Thread.setDefaultUncaughtExceptionHandler((Thread t, Throwable e) -> {

//            if(e instanceof Exception){
//                TaskManager.INSTANCE.submit(() -> {throw (Exception) e;});
//            }
//            else{
                RhomeoCore.LOGGER.log(Level.WARNING, "A throwable has not been caught.", e);
//            }
        });

        // perform initialization and plugin loading tasks
        final InitialLoadingTask initTask = new InitialLoadingTask();
        showLoadingStage(initTask);
        TaskManager.INSTANCE.submit(initTask);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void stop() throws Exception {
        try {
            Session.getInstance().close();
        } catch (IllegalStateException ex) {
            // The context was not already initialized, no need to close it.
            // Not a real exception since nothing to do
        }

        TaskManager.INSTANCE.close();
    }

    /**
     * Display splash screen.
     *
     * @param task the related task to show progress
     * @throws IOException
     */
    private void showLoadingStage(final InitialLoadingTask task) throws IOException {
        // Initialize splash screen
        final Stage splashStage = Rhomeo.newStage();
        splashStage.initStyle(StageStyle.TRANSPARENT);

        final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fr/cenra/rhomeo/fx/FXSplashScreen.fxml"));
        final GridPane root = loader.load();
        final FXSplashScreen splashScreen = loader.getController();
        splashScreen.uiCancel.setVisible(false);
        splashScreen.uiProgressLabel.textProperty().bind(task.messageProperty());
        splashScreen.uiProgressBar.progressProperty().bind(task.progressProperty());

        final Scene scene = new Scene(root);
        scene.getStylesheets().add("/fr/cenra/rhomeo/splashscreen.css");
        scene.setFill(new Color(0, 0, 0, 0));

        splashStage.setScene(scene);
        splashStage.show();

        // If the task succeeded, meaning the initialisation is finished or an update has been found
        // and user wants to download it.
        task.setOnSucceeded(evt -> Platform.runLater(() -> {
            final Object value = task.getValue();
            // If value is an URL, then it is an update to download.
            // Otherwise, nothing to download keep going with the application start.
            if (value instanceof URLAndMD5) {
                final DownloadUpdateTask downloadTask = new DownloadUpdateTask((URLAndMD5) value);
                splashScreen.uiProgressLabel.textProperty().unbind();
                splashScreen.uiProgressLabel.textProperty().bind(downloadTask.messageProperty());
                splashScreen.uiProgressBar.progressProperty().unbind();
                splashScreen.uiProgressBar.progressProperty().bind(downloadTask.progressProperty());

                TaskManager.INSTANCE.submit(downloadTask);

                downloadTask.setOnCancelled(evt2 -> System.exit(0));

                downloadTask.setOnFailed(evt2 -> Platform.runLater(() -> {
                        GeotkFX.newExceptionDialog("Le téléchargement de la mise à jour a échoué !", downloadTask.getException()).showAndWait();
                        System.exit(1);
                }));

                downloadTask.setOnSucceeded((evt2) -> {
                    Path newValue = downloadTask.getValue();
                    try {
                        Rhomeo.openFile(newValue);
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        RhomeoCore.LOGGER.log(Level.WARNING, "Cannot proceed to update !", e);
                    } finally {
                        Platform.runLater(() -> splashStage.close());
                    }
                });
            } else {
                // Launches the main window here
                splashStage.toFront();
                splashScreen.uiLoadingPane.setVisible(false);

                final FadeTransition fadeSplash = new FadeTransition(Duration.seconds(1.2), root);
                fadeSplash.setFromValue(1.0);
                fadeSplash.setToValue(0.0);
                fadeSplash.setOnFinished(actionEvent -> {
                    splashStage.hide();
                    root.setOpacity(1.0);
                    try {
                        final Stage mainStage = createDashboardStage();
                        final Preferences welcomePrefs = Preferences.userNodeForPackage(FXInitialDialogPane.class);

                        final Stage popup;
                        if (welcomePrefs.getBoolean(FXInitialDialogPane.DISPLAY_RULE_KEY, true)) {
                            popup = Rhomeo.createWelcomePopup();
                            popup.initOwner(mainStage);
                            popup.show();
                            popup.requestFocus();
                        } else {
                            popup = null;
                        }

                        // HACK : display of both stages is done now, because if
                        // main stage is displayed before we've ended configuring
                        // popup, some properties (like resizing capabilities)
                        // looks shared.
                        mainStage.show();
                        if (popup != null) {
                            popup.show();
                            popup.requestFocus();
                        }

                    } catch (Throwable ex) {
                        try {
                            RhomeoCore.LOGGER.log(Level.WARNING, "Erreur inattendue lors de l'initialisation du panneau principal.", ex);
                            final ExceptionDialog exDialog = GeotkFX.newExceptionDialog("L'application a rencontré une erreur inattendue et doit fermer.", ex);
                            exDialog.setOnHidden((DialogEvent de) -> System.exit(1));
                            exDialog.show();
                        } catch (Throwable e) {
                            RhomeoCore.LOGGER.log(Level.WARNING, "Cannot show error dialog to user", e);
                            System.exit(1);
                        }
                    }
                });
                fadeSplash.play();
            }
        }));

        task.setOnCancelled(evt -> {
            System.exit(0);
        });

        // Task cancelled, because of an error or a user cancellation action.
        task.setOnFailed(evt -> {
            splashScreen.uiProgressLabel.getStyleClass().remove("label");
            splashScreen.uiProgressLabel.getStyleClass().add("label-error");
            splashScreen.uiCancel.setVisible(true);
        });
    }

    /**
     * Show the updates confirmation window. User is asked if the update should be downloaded.
     *
     * @param newVersion The update version.
     * @return {@code true} if the user wants to apply the update, {@code false} otherwise.
     * @throws ExecutionException
     * @throws InterruptedException
     */
    private boolean showUpdatesStage(final UpdateInfo newVersion) throws ExecutionException, InterruptedException {
        // Now that we found that an update is available, we can ask the user if the update should be downloaded.
        final Task<Boolean> askForUpdate = new Task<Boolean>() {

            @Override
            protected Boolean call() throws Exception {
                final StringBuilder builder = new StringBuilder()
                        .append("Version installée : ").append(Rhomeo.getVersion())
                        .append(System.lineSeparator())
                        .append("Version de la mise à jour : ").append(newVersion.getVersion())
                        .append(System.lineSeparator())
                        .append("Date de publication : ").append(newVersion.getDate() == null ? "Inconnue" : newVersion.getDate())
                        .append(System.lineSeparator());

                final String[] releaseNote = newVersion.getReleaseNote();
                if (releaseNote != null && releaseNote.length > 0) {
                    builder.append("Release-note : ");
                    for (final String str : releaseNote) {
                        builder.append(System.lineSeparator()).append(str);
                    }
                }

                final Alert alert = new Alert(Alert.AlertType.CONFIRMATION,
                        builder.toString(), ButtonType.NO, ButtonType.YES);
                alert.setHeaderText("Une mise à jour est disponible. Télécharger la mise à jour ?");
                final TextArea infoArea = new TextArea(builder.toString());
                infoArea.setEditable(false);
                infoArea.setPrefSize(666, 444);
                alert.setResizable(true);
                alert.getDialogPane().setContent(infoArea);
                final Optional<ButtonType> choice = alert.showAndWait();

                return ButtonType.YES.equals(choice.orElse(ButtonType.NO));
            }
        };
        Platform.runLater(() -> askForUpdate.run());
        return Boolean.TRUE.equals(askForUpdate.get());
    }


    /**
     * Main dashboard stage. Will be opened when the application will be launched.
     *
     * @return The main stage, never {@code null}
     */
    private Stage createDashboardStage() {
        final Stage mainStage = Rhomeo.newStage();
        mainStage.setOnCloseRequest(event -> System.exit(0));
        final FXMainPane mainPane = Session.getInstance().getBean(FXMainPane.class);
        final Scene scene = new Scene(mainPane);
        scene.getStylesheets().add(Rhomeo.CSS_THEME_PATH);
        mainStage.setScene(scene);
        mainStage.initModality(Modality.NONE);
        mainStage.setMinWidth(800);
        mainStage.setMinHeight(600);
        mainStage.setResizable(true);
        mainStage.setMaximized(true);
        return mainStage;
    }

    /**
     *
     * @return
     */
    private URLAndMD5 checkUpdate() throws Exception {
        final UpdateInfo newVersion = RhomeoCore.existingUpdate();
        if (newVersion != null) {
            final URL updateURL;
            final String md5;
            if (PlatformUtil.isWindows()) {
                updateURL = newVersion.getWin32();
                md5 = newVersion.getWin32MD5();
            } else if (PlatformUtil.isMac()) {
                updateURL = newVersion.getMacOS64();
                md5 = newVersion.getMacOS64MD5();
            } else if (PlatformUtil.isLinux()) {
                if (newVersion.getDeb64() != null && isDeb()) {
                    updateURL = newVersion.getDeb64();
                    md5 = newVersion.getDeb64MD5();
                } else {
                    updateURL = newVersion.getRpm64();
                    md5 = newVersion.getRpm64MD5();
                }
            } else {
                updateURL = null;
                md5 = null;
            }

            if (updateURL != null && showUpdatesStage(newVersion)) {
                return new URLAndMD5(updateURL, md5);
            }
        }

        return null;
    }

    private boolean isDeb() {
        try {
            return Runtime.getRuntime().exec("dpkg --version").waitFor() == 0;
        } catch (InterruptedException|IOException ex) {
            RhomeoCore.LOGGER.log(Level.WARNING, "Cannot detect if the system is a dpkg or RPM based OS.", ex);
        }

        return false;
    }

    /**
     * Initial loading task, to follow with a progress bar.
     * If an update has been detected, returns the download url.
     */
    private final class InitialLoadingTask extends Task {
        /**
         * Launched when this task will be called.
         *
         * @return Always return {@code null}.
         * @throws Exception
         */
        @Override
        protected Object call() throws Exception {
            final int total = 4;
            int inc = 0;

            try {
                // CHECK UPDATES ///////////////////////////////////////////////
                updateProgress(inc++, total);
                updateMessage("Vérification des mises à jour...");
                final URLAndMD5 upURL = checkUpdate();
                if (upURL != null) {
                    return upURL;
                }

                // EPSG DATABASE ///////////////////////////////////////////////
                updateProgress(inc++, total);
                updateMessage("Création de la base de données de projections cartographiques (EPSG) ...");
                // try to create it, won't do anything if already exist
                RhomeoCore.initEpsgDB();

                // SPRING CONTEXT //////////////////////////////////////////////
                updateProgress(inc++, total);
                updateMessage("Chargement du contexte de l'application ...");
                new ClassPathXmlApplicationContext(RhomeoCore.SPRING_CONTEXT_XML);

                // RESTORE APPLICATION STATE ///////////////////////////////////
                updateProgress(inc++, total);
                updateMessage("Récupération des données...");
                Session.getInstance().getBean(StateManager.class).restoreState();

                // END
                updateProgress(total, total);
                updateMessage("Chargement terminé");
                Thread.sleep(400);
            } catch (Throwable ex) {
                updateProgress(-1, -1);
                updateMessage("Erreur inattendue : ".concat(ex.getLocalizedMessage()));
                RhomeoCore.LOGGER.log(Level.WARNING, ex.getMessage(), ex);

                throw ex;
            }

            return null;
        }
    }

    /**
     * Application update downloader.
     * When completed, returns the {@linkplain Path downloaded file}.
     */
    private final class DownloadUpdateTask extends Task<Path> {
        private final URLAndMD5 url;

        public DownloadUpdateTask(final URLAndMD5 url) {
            this.url = url;
        }

        /**
         * Download the new update.
         *
         * @return The {@linkplain Path downloaded update}
         * @throws Exception
         */
        @Override
        protected Path call() throws Exception {
            updateMessage("Téléchargement de la mise à jour en cours ...");

            String completeURL = url.url.toExternalForm();
            final Matcher matcher = RhomeoCore.EXT_PATTERN.matcher(completeURL);

            final Path tmpFile = Files.createTempFile("tmpInstall", matcher.find()? matcher.group() : ".tmp");

            // Now download the update in the appropriate file
            final URLConnection bundleConnec = new NetPreferences().openConnection(url.url);
            bundleConnec.setConnectTimeout(RhomeoCore.CONNECTION_TIMEOUT);
            bundleConnec.setReadTimeout(RhomeoCore.READ_TIMEOUT);
            final long totalLength = bundleConnec.getContentLengthLong();
            long downloaded = 0;
            final String totalReadableSize = RhomeoCore.toReadableSize(totalLength);
            final MessageDigest digest = MessageDigest.getInstance("md5");
            try (final InputStream input = bundleConnec.getInputStream();
                 final OutputStream output = Files.newOutputStream(tmpFile)) {
                int readBytes;
                final byte[] buffer = new byte[4096];
                while ((readBytes = input.read(buffer)) >= 0) {
                    output.write(buffer, 0, readBytes);
                    digest.update(buffer, 0, readBytes);
                    downloaded += readBytes;
                    updateProgress(downloaded, totalLength);
                    updateMessage("Téléchargement "+ RhomeoCore.toReadableSize(downloaded) + " / " + totalReadableSize);
                }
                output.flush();
            }

            // Verify that the content was fully downloaded
            if (downloaded != totalLength) {
                throw new IOException("La mise à jour n'a pas été entièrement téléchargée");
            }

            // MD5 checksum
            if (url.md5Base64 != null && !url.md5Base64.isEmpty()) {
                byte[] computed = digest.digest();
                if (!url.md5Base64.equals(new BigInteger(1, computed).toString(16))) {
                    throw new IOException("Downloaded file checksum is invalid !");
                }
            }

            updateMessage("Mise à jour téléchargée. Veuillez cliquer sur le bouton et l'installer.");
            return tmpFile;
        }
    }

    private static class URLAndMD5 {
        final URL url;
        final String md5Base64;

        public URLAndMD5(final URL packageURL, final String base64MD5) {
            this.url = packageURL;
            this.md5Base64 = base64MD5;
        }
    }
}
