/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.channel.cc.commons.spring;

import static java.nio.file.Files.deleteIfExists;
import static java.nio.file.Files.isDirectory;
import static java.text.MessageFormat.format;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.Banner.Mode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.config.ConfigFileApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

/**
 * Utility class for working with {@link Application}.
 */
public final class Applications {
    private static final Logger LOGGER = LoggerFactory.getLogger("std");

    private Applications() {
    }

    /**
     * Runs an {@link Application} instance with the specified command-line
     * arguments. This is a shortcut for
     * {@code main(applicationClass, args, false)}.
     * 
     * @param applicationClass
     *            the application class
     * @param args
     *            the arguments.
     */
    public static void main(Class<? extends Application> applicationClass,
            String[] args) {
        main(applicationClass, args, false);
    }

    /**
     * Runs an {@link Application} instance with the specified command-line
     * arguments. The application class must define a valid Spring
     * configuration. The {@ code deleteConfig} determines whether a
     * configuration file defined by {@ --spring.config.location=...} option
     * should be deleted after start. This method exits the JVM.
     * 
     * @param applicationClass
     *            the application class
     * @param args
     *            the arguments
     * @param deleteConfig
     *            delete configuration file after start
     */
    public static void main(Class<? extends Application> applicationClass,
            String[] args, boolean deleteConfig) {
        System.exit(run(applicationClass, args, deleteConfig));
    }

    /**
     * Runs an {@link Application} instance with the specified command-line
     * arguments. This is a shortcut for
     * {@code run(applicationClass, args, false)}.
     * 
     * @param applicationClass
     *            the application class
     * @param args
     *            the arguments
     * @return the exit code.
     */
    public static int run(Class<? extends Application> applicationClass,
            String[] args) {
        return run(applicationClass, args, false);
    }

    /**
     * Runs an {@link Application} instance with the specified command-line
     * arguments. The application class must define a valid Spring
     * configuration. The {@ code deleteConfig} determines whether a
     * configuration file defined by {@ --spring.config.location=...} option
     * should be deleted after startThis method does not exit the JVM.
     * 
     * @param applicationClass
     *            the application class
     * @param args
     *            the arguments
     * @param deleteConfig
     *            delete configuration file after start
     * @return the exit code.
     */
    public static int run(Class<? extends Application> applicationClass,
            String[] args, boolean deleteConfig) {
        try {
            return doRun(applicationClass, args, deleteConfig);
        } catch (Throwable e) {
            LOGGER.error("Application failed.", e);
            return -1;
        }
    }

    private static void deleteConfigurationFile(
            ConfigurableApplicationContext context) {
        ConfigurableEnvironment environment = context.getEnvironment();
        String property = environment.getProperty(
            ConfigFileApplicationListener.CONFIG_LOCATION_PROPERTY);
        if (property == null) {
            return;
        }

        URI uri;
        try {
            uri = new URI(property);
        } catch (URISyntaxException e) {
            LOGGER.warn(format(
                "Configuration location ''{0}'' is not a valid URI.",
                property), e);
            return;
        }

        if (!"file".equals(uri.getScheme())) {
            LOGGER.warn(
                format("Configuration location ''{0}'' is not a file URI.",
                    property));
            return;
        }

        Path file = Paths.get(uri);
        if (isDirectory(file)) {
            LOGGER.warn(format(
                "Configuration file ''{0}'' is a directory.", file));
            return;
        }

        try {
            deleteIfExists(file);
        } catch (IOException e) {
            LOGGER.warn(format(
                "Failed to delete configuration file ''{0}''.", file), e);
        }
    }

    private static int doRun(Class<? extends Application> applicationClass,
            String[] args, boolean deleteConfig) {
        SpringApplication launcher =
            newSpringApplication(applicationClass);
        try (ConfigurableApplicationContext context = launcher.run(args)) {
            if (deleteConfig) {
                deleteConfigurationFile(context);
            }
            return doRun(context, args);
        }
    }

    private static int doRun(ConfigurableApplicationContext context,
            String[] args) {
        Application application = getApplication(context);
        Thread hook = new Thread(application::shutdown);
        Runtime.getRuntime().addShutdownHook(hook);
        try {
            application.run(args);
            return SpringApplication.exit(context);
        } catch (ApplicationException e) {
            LOGGER.error("Application failed.", e);
            return SpringApplication.exit(context, e.toArray());
        }
    }

    private static Application getApplication(
            ConfigurableApplicationContext context) {
        Map<String, Application> beans =
            context.getBeansOfType(Application.class);
        if (beans.isEmpty()) {
            throw new IllegalStateException("Application not found.");
        }
        return beans.values().iterator().next();
    }

    private static SpringApplication newSpringApplication(
            Class<?> configClass) {
        SpringApplicationBuilder builder =
            new SpringApplicationBuilder(configClass);
        builder.bannerMode(Mode.OFF);
        builder.headless(true);
        builder.addCommandLineProperties(true);
        builder.logStartupInfo(false);
        return builder.build();
    }
}
