/**
 *  Copyright 2005-2016 Red Hat, Inc.
 *
 *  Red Hat licenses this file to you under the Apache License, version
 *  2.0 (the "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 *  implied.  See the License for the specific language governing
 *  permissions and limitations under the License.
 */
package io.fabric8.utils;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * An ExecutorService which ensures serial execution of the Runnable
 * objects which it is asked to execute.  By default it delegates
 * execution of those tasks to a thread pool, but can be configured
 * to use any Executor.
 */
public class SerialExecutorService extends AbstractExecutorService {

    static long THREAD_POOL_KEEP_ALIVE = Integer.getInteger("io.fabric8.utils.THREAD_POOL_KEEP_ALIVE", 5000);
    static final ThreadGroup group = new ThreadGroup("Fabric Tasks");

    static final Executor threadPool = new Executor() {
        SynchronousQueue<Runnable> queue = new SynchronousQueue<Runnable>();

        @Override
        public void execute(final Runnable task) {

            if (task == null) {
                throw new NullPointerException();
            }

            // Lets try to give the task to a running thread..
            if (!queue.offer(task)) {

                // Existing thread did not take the task, so spin
                // up a thread to execute the task..
                new Thread(group, "Fabric Task") {
                    @Override
                    public void run() {
                        while (true) {
                            Runnable task;
                            try {
                                task = queue.poll(THREAD_POOL_KEEP_ALIVE, TimeUnit.MILLISECONDS);
                            } catch (InterruptedException e) {
                                return;
                            }
                            if (task == null) {
                                return;
                            }
                            task.run();
                        }
                    }
                }.start();

                // Now wait till a thread picks it up..
                try {
                    queue.put(task);
                } catch (InterruptedException e) {
                    throw new RejectedExecutionException(e);
                }
            }
        }
    };

    protected Executor target;
    protected volatile String label;
    protected AtomicBoolean shutdown = new AtomicBoolean(false);
    protected AtomicBoolean terminated = new AtomicBoolean(false);
    protected CountDownLatch terminatedLatch = new CountDownLatch(1);
    protected final AtomicBoolean triggered = new AtomicBoolean();
    protected final ConcurrentLinkedQueue<Runnable> externalQueue = new ConcurrentLinkedQueue<Runnable>();
    protected final LinkedList<Runnable> localQueue = new LinkedList<Runnable>();
    protected final ThreadLocal<Boolean> draining = new ThreadLocal<Boolean>();
    protected final Runnable drainTask = new Runnable() {
        public void run() {
            drain();
        }
    };


    public SerialExecutorService() {
        this("<no-label>");
    }

    public SerialExecutorService(String label) {
        this(threadPool, label);
    }

    public SerialExecutorService(Executor target) {
        this(target, "<no-label>");
    }

    public SerialExecutorService(Executor target, String label) {
        this.target = target;
        this.label = label;
    }

    /**
     * Queues the runnable for execution.
     * @param runnable
     */
    @Override
    public void execute(Runnable runnable) {
        if (runnable == null)
            throw new NullPointerException("runnable cannot be null");
        if (shutdown.get())
            throw new RejectedExecutionException("shutdown");

        if (isDraining()) {
            localQueue.add(runnable);
        } else {
            externalQueue.add(runnable);
            triggerDrain();
        }
    }

    /**
     * Executes the runnable.  This method blocks until it has been
     * executed.
     * @param runnable
     */
    public void executeAndDrain(Runnable runnable) {
        if (runnable == null)
            throw new NullPointerException("runnable cannot be null");
        if (shutdown.get())
            throw new RejectedExecutionException("shutdown");

        if (isDraining()) {
            runnable.run();
        } else {
            externalQueue.add(runnable);
            drain();
        }
    }

    protected void triggerDrain() {
        if (triggered.compareAndSet(false, true)) {
            target.execute(drainTask);
        }
    }

    /**
     * This method blocks until all previously queued Runnable objects are run.
     */
    synchronized public void drain() {
        draining.set(Boolean.TRUE);
        try {
            boolean drained = false;
            while (!drained) {
                Runnable runnable = localQueue.poll();
                if (runnable == null) {
                    runnable = externalQueue.poll();
                }
                if (runnable == null) {
                    drained = true;
                } else {
                    try {
                        runnable.run();
                    } catch (Throwable e) {
                        Thread thread = Thread.currentThread();
                        thread.getUncaughtExceptionHandler().uncaughtException(thread, e);
                    }
                }
            }
        } finally {
            draining.remove();
            triggered.set(false);
            if (!externalQueue.isEmpty()) {
                triggerDrain();
            }
        }
    }


    @Override
    public void shutdown() {
        if (shutdown.compareAndSet(false, true)) {
            externalQueue.add(new Runnable() {
                @Override
                public void run() {
                    terminated.set(true);
                    terminatedLatch.countDown();
                }
            });
            triggerDrain();
        }
    }

    @Override
    public List<Runnable> shutdownNow() {
        shutdown();
        return Collections.EMPTY_LIST;
    }

    public boolean isDraining() {
        return draining.get() == Boolean.TRUE;
    }


    @Override
    public boolean isShutdown() {
        return shutdown.get();
    }

    @Override
    public boolean isTerminated() {
        return terminated.get();
    }

    @Override
    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        return terminatedLatch.await(timeout, unit);
    }

    @Override
    public String toString() {
        return label;
    }

    public Executor getTarget() {
        return target;
    }

    public void setTarget(Executor target) {
        this.target = target;
    }

    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        this.label = label;
    }
}
