package com.asteria.service;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy;
import java.util.concurrent.TimeUnit;

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

/**
 * The class that manages an internal queue of {@link Service}s within a
 * {@link ScheduledExecutorService}. The primary use for this class is to make
 * executing scheduled asynchronous tasks more manageable.
 * <p>
 * <p>
 * Please keep in mind that even though the management of services is
 * synchronized, that the code within the services must be thread safe or race
 * conditions and other concurrency issues can occur.
 * 
 * @author lare96 <http://github.org/lare96>
 */
public final class ServiceQueue {

    /**
     * The executor service that will allow us to execute various
     * {@link Service}s asynchronously.
     */
    private final ScheduledExecutorService executor;

    /**
     * Creates a new {@link ServiceQueue}.
     *
     * @param executor
     *            the executor to run this service queue with.
     */
    public ServiceQueue(ScheduledExecutorService executor) {
        this.executor = executor;
    }

    /**
     * Creates a new {@link ServiceQueue} with a timeout value of
     * {@code seconds}.
     */
    public ServiceQueue(long seconds) {
        this(createServiceExecutor(seconds));
    }

    /**
     * Creates a new {@link ServiceQueue} with a timeout value of {@code 0},
     * meaning this service queue will never go idle.
     */
    public ServiceQueue() {
        this(createServiceExecutor(0));
    }

    /**
     * Submits {@code service} to this service queue to be executed by the
     * internal executor either as a scheduled or direct service. A direct
     * service has a delay of {@code 0} and a scheduled has a delay of above
     * that.
     *
     * @param service
     *            the service to submit to this service queue.
     */
    public void submit(Service service) {
        Preconditions.checkState(!executor.isShutdown(), "Cannot submit services to a ServiceQueue that has been shutdown.");
        Preconditions.checkArgument(!service.isDone(), "Cannot submit cancelled services to this ServiceQueue.");
        if (service.getDelay() == 0) {
            submitDirect(service);
        } else {
            submitScheduled(service);
        }
    }

    /**
     * Submits a scheduled {@code service} to this service queue to be executed
     * by the internal executor.
     *
     * @param service
     *            the service to submit to this service queue.
     */
    private void submitScheduled(Service service) {
        service.setFuture(executor.scheduleAtFixedRate(() -> {
            try {
                service.execute(this);
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }, service.getDelay(), service.getDelay(), TimeUnit.MILLISECONDS));
    }

    /**
     * Submits a direct {@code service} to this service queue to be executed by
     * the internal executor.
     *
     * @param service
     *            the service to submit to this service queue.
     */
    private void submitDirect(Service service) {
        service.setFuture(executor.submit(() -> {
            try {
                service.execute(this);
                service.cancel();
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }));
    }

    /**
     * Attempts to gracefully terminate this service queue. After this function
     * returns, this service queue will not be able to be used.
     * <p>
     * <p>
     * Please note that this function does indeed block the underlying thread
     * until {@code 5} seconds pass or all services complete.
     */
    public void terminate() {
        try {
            executor.shutdownNow();
            executor.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException ignored) {}
    }

    /**
     * Creates and configures a new {@link ScheduledExecutorService} with a
     * timeout value of {@code seconds}. If the timeout value is below or equal
     * to zero then the returned executor will never timeout.
     *
     * @return the newly created and configured executor service.
     */
    private static ScheduledExecutorService createServiceExecutor(long seconds) {
        Preconditions.checkArgument(seconds >= 0, "The timeout value must be equal to or greater than 0!");
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
        executor.setRejectedExecutionHandler(new CallerRunsPolicy());
        executor.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("ServiceQueueThread").build());
        if (seconds > 0) {
            executor.setKeepAliveTime(seconds, TimeUnit.SECONDS);
            executor.allowCoreThreadTimeOut(true);
        }
        return executor;
    }
}