package info.dong4j.idea.plugin.sdk.qcloud.cos.event;

import org.apache.commons.logging.LogFactory;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;

import static info.dong4j.idea.plugin.sdk.qcloud.cos.event.ProgressEventType.HTTP_REQUEST_CONTENT_RESET_EVENT;
import static info.dong4j.idea.plugin.sdk.qcloud.cos.event.ProgressEventType.HTTP_RESPONSE_CONTENT_RESET_EVENT;
import static info.dong4j.idea.plugin.sdk.qcloud.cos.event.ProgressEventType.REQUEST_BYTE_TRANSFER_EVENT;
import static info.dong4j.idea.plugin.sdk.qcloud.cos.event.ProgressEventType.REQUEST_CONTENT_LENGTH_EVENT;
import static info.dong4j.idea.plugin.sdk.qcloud.cos.event.ProgressEventType.RESPONSE_BYTE_TRANSFER_EVENT;
import static info.dong4j.idea.plugin.sdk.qcloud.cos.event.ProgressEventType.RESPONSE_CONTENT_LENGTH_EVENT;

/**
 * This class is responsible for executing the callback method of
 * ProgressListener; listener callbacks are executed sequentially in a separate
 * single thread.
 */
public class SDKProgressPublisher {
    protected static final boolean SYNC = false;  // for testing purposes only
    protected static final boolean ASYNC = false; // for testing purposes only
    /**
     * Used for testing purposes only.
     */
    private static volatile Future<?> latestFutureTask;

    /**
     * Used to deliver a progress event to the given listener.
     *
     * @return the future of a submitted task; or null if the delivery is
     * synchronous with no future task involved.  Note a listener should never
     * block, and therefore returning null is the typical case.
     */
    public static Future<?> publishProgress(
            final ProgressListener listener,
            final ProgressEventType type) {
        if (listener == ProgressListener.NOOP || listener == null
        ||  type == null) {
            return null;
        }
        return deliverEvent(listener, new ProgressEvent(type));
    }

    private static Future<?> deliverEvent(final ProgressListener listener,
            final ProgressEvent event) {
        if (SYNC) { // forces all callbacks to be made synchronously
            return quietlyCallListener(listener, event);
        }
        if (!ASYNC) { // forces all callbacks to be made asynchronously
            if (listener instanceof DeliveryMode) {
                DeliveryMode mode = (DeliveryMode) listener;
                if (mode.isSyncCallSafe()) {
                    // Safe to call the listener directly
                    return quietlyCallListener(listener, event);
                }
            }
        }
        // Not safe to call the listener directly; so submit an async task.
        // This is unfortunate as the listener should never block in the first
        // place, but such task submission is necessary to remain backward
        // compatible.
        return latestFutureTask = LazyHolder.executor.submit(new Runnable() {
            @Override
            public void run() {
                listener.progressChanged(event);
            }
        });
    }

    private static Future<?> quietlyCallListener(final ProgressListener listener,
            final ProgressEvent event) {
        try {
            listener.progressChanged(event);
        } catch(Throwable t) {
            // That's right, we need to suppress all errors so as to be on par
            // with the async mode where all failures will be ignored.
            LogFactory.getLog(SDKProgressPublisher.class)
                .debug("Failure from the event listener", t);
        }
        return null;
    }

    /**
     * Convenient method to publish a request content length event to the given
     * listener.
     *
     * @param listener
     *            must not be null or else the publication will be skipped
     * @param bytes
     *            must be non-negative or else the publication will be skipped
     */
    public static Future<?> publishRequestContentLength(
            final ProgressListener listener,
            final long bytes) {
        return publishByteCountEvent(listener, REQUEST_CONTENT_LENGTH_EVENT, bytes);
    }

    /**
     * Convenient method to publish a response content length event to the given
     * listener.
     *
     * @param listener
     *            must not be null or else the publication will be skipped
     * @param bytes
     *            must be non-negative or else the publication will be skipped
     */
    public static Future<?> publishResponseContentLength(
            final ProgressListener listener,
            final long bytes) {
        return publishByteCountEvent(listener, RESPONSE_CONTENT_LENGTH_EVENT, bytes);
    }

    /**
     * Convenient method to publish a request byte transfer event to the given
     * listener.
     *
     * @param listener
     *            must not be null or else the publication will be skipped
     * @param bytes
     *            must be non-negative or else the publication will be skipped
     */
    public static Future<?> publishRequestBytesTransferred(
            final ProgressListener listener,
            final long bytes) {
        return publishByteCountEvent(listener, REQUEST_BYTE_TRANSFER_EVENT, bytes);
    }

    /**
     * Convenient method to publish a response byte transfer event to the given
     * listener.
     *
     * @param listener
     *            must not be null or else the publication will be skipped
     * @param bytes
     *            must be non-negative or else the publication will be skipped
     */
    public static Future<?> publishResponseBytesTransferred(
            final ProgressListener listener,
            final long bytes) {
        return publishByteCountEvent(listener, RESPONSE_BYTE_TRANSFER_EVENT, bytes);
    }

    private static Future<?> publishByteCountEvent(
            final ProgressListener listener,
            final ProgressEventType type,
            final long bytes) {
        if (listener == ProgressListener.NOOP || listener == null || bytes <= 0)
            return null;
        return deliverEvent(listener, new ProgressEvent(type, bytes));
    }

    /**
     * Convenient method to publish a request reset event to the given listener.
     *
     * @param listener
     *            must not be null or else the publication will be skipped
     * @param bytesReset
     *            must be non-negative or else the publication will be skipped
     */
    public static Future<?> publishRequestReset(
            final ProgressListener listener,
            final long bytesReset) {
        return publishResetEvent(listener, HTTP_REQUEST_CONTENT_RESET_EVENT, bytesReset);
    }

    /**
     * Convenient method to publish a response reset event to the given listener.
     */
    public static Future<?> publishResponseReset(
            final ProgressListener listener,
            final long bytesReset) {
        return publishResetEvent(listener, HTTP_RESPONSE_CONTENT_RESET_EVENT, bytesReset);
    }

    /**
     * Convenient method to publish a response bytes discard event to the given listener.
     */
    public static Future<?> publishResponseBytesDiscarded(
            final ProgressListener listener,
            final long bytesDiscarded) {
        return publishResetEvent(listener,
                ProgressEventType.RESPONSE_BYTE_DISCARD_EVENT, bytesDiscarded);
    }

    /**
     * @param listener
     *            must not be null or else the publication will be skipped
     * @param bytesReset
     *            the publication will be skipped unless the number of bytes
     *            reset is positive
     */
    private static Future<?> publishResetEvent(
            final ProgressListener listener,
            final ProgressEventType resetEventType,
            final long bytesReset) {
        if (bytesReset <= 0)
            return null;
        if (listener == ProgressListener.NOOP || listener == null)
            return null;
        return deliverEvent(listener, new ProgressEvent(resetEventType, bytesReset));
    }

    /**
     * Returns the executor service used for performing the callbacks.
     */
    protected static ExecutorService getExecutorService() {
        return LazyHolder.executor;
    }

    protected static Future<?> setLatestFutureTask(Future<?> f) {
        return latestFutureTask = f;
    }

    /**
     * For internal testing and backward compatibility only. This method blocks
     * until all the submitted callbacks are executed. Listeners should never
     * block so this method should never be used.
     */
    @Deprecated
    public static void waitTillCompletion()
            throws InterruptedException, ExecutionException {
        if (latestFutureTask != null) {
            latestFutureTask.get();
        }
    }

    /**
     * Used to avoid creating the extra thread until absolutely necessary.
     */
    private static final class LazyHolder {
        /** A single thread pool for executing all ProgressListener callbacks. **/
        private static final ExecutorService executor = createNewExecutorService();

        /**
         * Creates a new single threaded executor service for performing the
         * callbacks.
         */
        private static ExecutorService createNewExecutorService() {
            return Executors.newSingleThreadExecutor(new ThreadFactory() {
                public Thread newThread(Runnable r) {
                    Thread t = new Thread(r);
                    t.setName("java-sdk-progress-listener-callback-thread");
                    t.setDaemon(true);
                    return t;
                }
            });
        }
    }

    /**
     * Can be used to shutdown the (legacy) executor.
     * <p>
     * However, the recommended best practice is to always make use of progress
     * listeners that are short-lived (ie do not block) and are subclasses of
     * {@link SyncProgressListener}. That way, the progress publisher
     * (legacy) thread will never be activated in the first place.
     *
     * @param now true if shutdown now; false otherwise.
     */
    public static void shutdown(boolean now) {
        if (now)
            LazyHolder.executor.shutdownNow();
        else
            LazyHolder.executor.shutdown();
    }
}