package com.atlassian.bitbucket.util.concurrent;

import com.google.common.base.MoreObjects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Utilities for managing {@link ExecutorService}s.
 */
public class ExecutorUtils {

    private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(ExecutorUtils.class);
    private static final long DEFAULT_SHUTDOWN_TIME = 10;
    private static final long DEFAULT_SHUTDOWN_NOW_TIME = 5;
    private static final TimeUnit DEFAULT_SHUTDOWN_TIME_UNIT = TimeUnit.SECONDS;
    private static final TimeUnit DEFAULT_SHUTDOWN_NOW_TIME_UNIT = TimeUnit.SECONDS;

    private ExecutorUtils() {
        throw new UnsupportedOperationException(getClass().getName() +
                " is a utility class and should not be instantiated");
    }

    /**
     * Shutdown an {@link ExecutorService} politely using the default log and shutdown times.
     *
     * @param executorService the service to be shut down
     */
    public static void shutdown(@Nonnull ExecutorService executorService) {
        shutdown(executorService, DEFAULT_LOG);
    }

    /**
     * Shutdown an {@link ExecutorService} politely using the default log.
     *
     * @param executorService     the service to be shut down
     * @param shutdownTime        the time to wait for {@link ExecutorService#awaitTermination(long, TimeUnit)}
     *                            to finish before forcing shutdown
     * @param shutdownTimeUnit    the {@link TimeUnit} of shutdownTime
     * @param shutdownNowTime     the time time to wait for {@link ExecutorService#shutdownNow()} to finish
     * @param shutdownNowTimeUnit the {@link TimeUnit} of shutdownNowTime
     */
    public static void shutdown(@Nonnull ExecutorService executorService,
                                long shutdownTime,
                                @Nonnull TimeUnit shutdownTimeUnit,
                                long shutdownNowTime,
                                @Nonnull TimeUnit shutdownNowTimeUnit) {
        shutdown(executorService, shutdownTime, shutdownTimeUnit, shutdownNowTime, shutdownNowTimeUnit, DEFAULT_LOG);
    }

    /**
     * Shutdown an {@link ExecutorService} politely using the default shutdown times.
     *
     * @param executorService the service to be shut down
     * @param log             the log to use for log messages
     */
    public static void shutdown(@Nonnull ExecutorService executorService, @Nonnull Logger log) {
        shutdown(executorService,
                DEFAULT_SHUTDOWN_TIME,
                DEFAULT_SHUTDOWN_TIME_UNIT,
                DEFAULT_SHUTDOWN_NOW_TIME,
                DEFAULT_SHUTDOWN_NOW_TIME_UNIT,
                log);
    }

    /**
     * Shutdown an {@link ExecutorService} politely.
     *
     * @param executorService     the service to be shut down
     * @param shutdownTime        the time to wait for {@link ExecutorService#awaitTermination(long, TimeUnit)}
     *                            to finish before forcing shutdown
     * @param shutdownTimeUnit    the {@link TimeUnit} of shutdownTime
     * @param shutdownNowTime     the time time to wait for {@link ExecutorService#shutdownNow()} to finish
     * @param shutdownNowTimeUnit the {@link TimeUnit} of shutdownNowTime
     * @param log                 the log to use for log messages
     */
    public static void shutdown(@Nonnull ExecutorService executorService,
                                long shutdownTime,
                                @Nonnull TimeUnit shutdownTimeUnit,
                                long shutdownNowTime,
                                @Nonnull TimeUnit shutdownNowTimeUnit,
                                @Nonnull Logger log) {
        checkNotNull(executorService, "executorService");
        shutdownTimeUnit = MoreObjects.firstNonNull(shutdownTimeUnit, DEFAULT_SHUTDOWN_NOW_TIME_UNIT);
        shutdownNowTimeUnit = MoreObjects.firstNonNull(shutdownNowTimeUnit, DEFAULT_SHUTDOWN_NOW_TIME_UNIT);
        log = MoreObjects.firstNonNull(log, DEFAULT_LOG);

        try {
            executorService.shutdown();
            if (executorService.awaitTermination(shutdownTime, shutdownTimeUnit)) {
                log.debug("Executor service has shutdown gracefully");
            } else {
                //The executor did not shutdown within the timeout. We can't wait forever, though, so issue a
                //shutdownNow() and give it another 5 seconds
                log.warn("Executor service did not shutdown within the timeout; forcing shutdown");

                executorService.shutdownNow();
                if (executorService.awaitTermination(shutdownNowTime, shutdownNowTimeUnit)) {
                    //The forced shutdown has brought the executor down. Not ideal, but acceptable
                    log.debug("Executor service has been forced to shutdown");
                } else {
                    //We can't delay execution indefinitely waiting, so log a warning. The JVM may not shut down
                    //if this service does not stop (because it uses non-daemon threads), so this may be helpful
                    //in debugging should that happen.
                    log.warn("Executor service did not shutdown; it will be abandoned");
                }
            }
        } catch (InterruptedException e) {
            log.warn("Interrupted while waiting for the executor service to shutdown; some worker threads may " +
                    "still be running");
            Thread.currentThread().interrupt();
        }
    }
}
