package fxlauncher;

import com.sun.javafx.application.PlatformImpl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.TextArea;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.concurrent.CountDownLatch;

@SuppressWarnings("unchecked")
public class Launcher extends Application {
    private static final Logger log = Logger.getLogger("Launcher");

    private Application app;
    private Stage primaryStage;
    private Stage stage;
    private UIProvider uiProvider;
    private StackPane root;

    private final AbstractLauncher superLauncher = new AbstractLauncher<Application>() {
        @Override
        protected Parameters getParameters() {
            return Launcher.this.getParameters();
        }

        @Override
        protected void updateProgress(double progress) {
            Platform.runLater(() -> uiProvider.updateProgress(progress));
        }

        @Override
        protected void createApplication(Class<Application> appClass) {
            runAndWait(() ->
            {
                try {
                    if (Application.class.isAssignableFrom(appClass)) {
                        app = appClass.newInstance();
                    } else {
                        throw new IllegalArgumentException(String.format("Supplied appClass %s was not a subclass of javafx.application.Application!", appClass));
                    }
                } catch (Throwable t) {
                    reportError("Error creating app class", t);
                }
            });
        }

        @Override
        protected void reportError(String title, Throwable error) {
            log.log(Level.WARNING, title, error);

            Platform.runLater(() ->
            {
                Alert alert = new Alert(Alert.AlertType.ERROR);
                alert.setTitle(title);
                alert.setHeaderText(String.format("%s\ncheck the logfile 'fxlauncher.log, usually in the %s directory", title, System.getProperty("java.io.tmpdir")));
//            alert.setHeaderText(title+"\nCheck the logfile usually in the "+System.getProperty("java.io.tmpdir") + "directory");
                alert.getDialogPane().setPrefWidth(600);

                ByteArrayOutputStream out = new ByteArrayOutputStream();
                PrintWriter writer = new PrintWriter(out);
                error.printStackTrace(writer);
                writer.close();
                TextArea text = new TextArea(out.toString());
                alert.getDialogPane().setContent(text);

                alert.showAndWait();
                Platform.exit();
            });
        }

        @Override
        protected void setupClassLoader(ClassLoader classLoader) {
            FXMLLoader.setDefaultClassLoader(classLoader);
            Platform.runLater(() -> Thread.currentThread().setContextClassLoader(classLoader));
        }


    };

    /**
     * Check if a new version is available and return the manifest for the new version or null if no update.
     * <p>
     * Note that updates will only be detected if the application was actually launched with FXLauncher.
     *
     * @return The manifest for the new version if available
     */
    public static FXManifest checkForUpdate() throws IOException {
        // We might be called even when FXLauncher wasn't used to start the application
        if (AbstractLauncher.manifest == null) return null;
        FXManifest manifest = FXManifest.load(URI.create(AbstractLauncher.manifest.uri + "/app.xml"));
        return manifest.equals(AbstractLauncher.manifest) ? null : manifest;
    }


    /**
     * Initialize the UI Provider by looking for an UIProvider inside the launcher
     * or fallback to the default UI.
     * <p>
     * A custom implementation must be embedded inside the launcher jar, and
     * /META-INF/services/fxlauncher.UIProvider must point to the new implementation class.
     * <p>
     * You must do this manually/in your build right around the "embed manifest" step.
     */
    public void init() throws Exception {
        Iterator<UIProvider> providers = ServiceLoader.load(UIProvider.class).iterator();
        uiProvider = providers.hasNext() ? providers.next() : new DefaultUIProvider();
    }

    public void start(Stage primaryStage) throws Exception {
        this.primaryStage = primaryStage;
        stage = new Stage(StageStyle.UNDECORATED);
        root = new StackPane();
        final boolean[] filesUpdated = new boolean[1];

        Scene scene = new Scene(root);
        stage.setScene(scene);

        superLauncher.setupLogFile();
        superLauncher.checkSSLIgnoreflag();
        this.uiProvider.init(stage);
        root.getChildren().add(uiProvider.createLoader());

        stage.show();

        new Thread(() -> {
            Thread.currentThread().setName("FXLauncher-Thread");
            try {
                superLauncher.updateManifest();
                createUpdateWrapper();
                filesUpdated[0] = superLauncher.syncFiles();
            } catch (Exception ex) {
                log.log(Level.WARNING, String.format("Error during %s phase", superLauncher.getPhase()), ex);
                if (superLauncher.checkIgnoreUpdateErrorSetting()) {
                    superLauncher.reportError(String.format("Error during %s phase", superLauncher.getPhase()), ex);
                    System.exit(1);
                }
            }

            try {
                superLauncher.createApplicationEnvironment();
                launchAppFromManifest(filesUpdated[0]);
            } catch (Exception ex) {
                superLauncher.reportError(String.format("Error during %s phase", superLauncher.getPhase()), ex);
            }

        }).start();
    }

    private void launchAppFromManifest(boolean showWhatsnew) throws Exception {
        superLauncher.setPhase("Application Environment Prepare");

        try {
            initApplication();
        } catch (Throwable ex) {
            superLauncher.reportError("Error during app init", ex);
        }
        superLauncher.setPhase("Application Start");
        log.info("Show whats new dialog? " + showWhatsnew);

        runAndWait(() ->
        {
            try {
                if (showWhatsnew && superLauncher.getManifest().whatsNewPage != null)
                    showWhatsNewDialog(superLauncher.getManifest().whatsNewPage);

                // Lingering update screen will close when primary stage is shown
                if (superLauncher.getManifest().lingeringUpdateScreen) {
                    primaryStage.showingProperty().addListener(observable -> {
                        if (stage.isShowing())
                            stage.close();
                    });
                } else {
                    stage.close();
                }

                startApplication();
            } catch (Throwable ex) {
                superLauncher.reportError("Failed to start application", ex);
            }
        });
    }

    private void showWhatsNewDialog(String whatsNewURL) {
        WebView view = new WebView();
        view.getEngine().load(whatsNewURL);
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setTitle("What's new");
        alert.setHeaderText("New in this update");
        alert.getDialogPane().setContent(view);
        alert.showAndWait();
    }

    public static void main(String[] args) {
        launch(args);
    }

    private void createUpdateWrapper() {
        superLauncher.setPhase("Update Wrapper Creation");

        Platform.runLater(() ->
        {
            Parent updater = uiProvider.createUpdater(superLauncher.getManifest());
            root.getChildren().clear();
            root.getChildren().add(updater);
        });
    }

    public void stop() throws Exception {
        if (app != null)
            app.stop();
    }

    private void initApplication() throws Exception {
        if (app != null) {
            app.init();
        }
    }

    private void startApplication() throws Exception {
        if (app != null) {
            final LauncherParams params = new LauncherParams(getParameters(), superLauncher.getManifest());
            app.getParameters().getNamed().putAll(params.getNamed());
            app.getParameters().getRaw().addAll(params.getRaw());
            app.getParameters().getUnnamed().addAll(params.getUnnamed());

            PlatformImpl.setApplicationName(app.getClass());
            superLauncher.setPhase("Application Init");
            app.start(primaryStage);
        } else {
            // Start any executable jar (i.E. Spring Boot);
            String firstFile = superLauncher.getManifest().files.get(0).file;
            log.info(String.format("No app class defined, starting first file (%s)", firstFile));
            Path cacheDir = superLauncher.getManifest().resolveCacheDir(getParameters().getNamed());
            String command = String.format("java -jar %s/%s", cacheDir.toAbsolutePath(), firstFile);
            log.info(String.format("Execute command '%s'", command));
            Runtime.getRuntime().exec(command);
        }
    }


    /**
     * Runs the specified {@link Runnable} on the
     * JavaFX application thread and waits for completion.
     *
     * @param action the {@link Runnable} to run
     * @throws NullPointerException if {@code action} is {@code null}
     */
    void runAndWait(Runnable action) {
        if (action == null)
            throw new NullPointerException("action");

        // run synchronously on JavaFX thread
        if (Platform.isFxApplicationThread()) {
            action.run();
            return;
        }

        // queue on JavaFX thread and wait for completion
        final CountDownLatch doneLatch = new CountDownLatch(1);
        Platform.runLater(() -> {
            try {
                action.run();
            } finally {
                doneLatch.countDown();
            }
        });

        try {
            doneLatch.await();
        } catch (InterruptedException e) {
            // ignore exception
        }
    }
}