/**
 * Copyright (c) 2014-2016 by the respective copyright holders.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.eclipse.smarthome.core.common;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is a thread pool executor service, which works as a developer would expect it to work.
 * The default {@link ThreadPoolExecutor} does the following (see
 * <a href="http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html">the official
 * JavaDoc)</a>:
 * <ul>
 * <li>If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than
 * queuing.</li>
 * <li>If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a
 * new thread.</li>
 * <li>If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case,
 * the task will be rejected.</li>
 * </ul>
 *
 * This class in contrast implements the following logic:
 * <ul>
 * <li>corePoolSize is 1, so threads are only created on demand</li>
 * <li>If the number of busy threads is smaller than the threadPoolSize, the Executor always prefers adding (or reusing)
 * a thread rather than queuing it.</li>
 * <li>If threadPoolSize threads are busy, new requests will be put in a FIFO queue and processed as soon as a thread
 * becomes idle.</li>
 * <li>The queue size is unbound, i.e. requests will never be rejected.
 * <li>Threads are terminated after being idle for at least 10 seconds.
 * </ul>
 * Please note that this implementation (with its partially hard-coded settings) is specifically targeted for use
 * on embedded devices without a high throughput. If you intend to use it for mass data processing on a server, you
 * should definitely tweak those settings.
 *
 * @author Kai Kreuzer - Initial contribution and API
 *
 */
public class QueueingThreadPoolExecutor extends ThreadPoolExecutor {

    private Logger logger = LoggerFactory.getLogger(QueueingThreadPoolExecutor.class);

    /** we will use a core pool size of 1 since we allow to timeout core threads. */
    final static int CORE_THREAD_POOL_SIZE = 1;

    /** Our queue for queueing tasks that wait for a thread to become available */
    private LinkedTransferQueue<Runnable> taskQueue = new LinkedTransferQueue<>();

    /** The thread for processing the queued tasks */
    private Thread queueThread;

    final private Object semaphore = new Object();

    final private String threadPoolName;

    /**
     * Allows to subclass QueueingThreadPoolExecutor.
     */
    protected QueueingThreadPoolExecutor(String name, int threadPoolSize) {
        this(name, new CommonThreadFactory(name), threadPoolSize,
                new QueueingThreadPoolExecutor.QueueingRejectionHandler());
    }

    private QueueingThreadPoolExecutor(String threadPoolName, ThreadFactory threadFactory, int threadPoolSize,
            RejectedExecutionHandler rejectionHandler) {
        super(CORE_THREAD_POOL_SIZE, threadPoolSize, 10L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
                threadFactory, rejectionHandler);
        this.threadPoolName = threadPoolName;
        allowCoreThreadTimeOut(true);
    }

    /**
     * Creates a new instance of {@link QueueingThreadPoolExecutor}
     *
     * @param name the name of the thread pool, will be used as a prefix for the name of the threads
     * @param threadPoolSize the maximum size of the pool
     * @return the {@link QueueingThreadPoolExecutor} instance
     */
    public static QueueingThreadPoolExecutor createInstance(String name, int threadPoolSize) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("A thread pool name must be provided!");
        }
        return new QueueingThreadPoolExecutor(name, new CommonThreadFactory(name), threadPoolSize,
                new QueueingThreadPoolExecutor.QueueingRejectionHandler());
    }

    /**
     * Adds a new task to the queue
     *
     * @param runnable the task to add
     */
    protected void addToQueue(Runnable runnable) {
        taskQueue.add(runnable);

        if (queueThread == null || !queueThread.isAlive()) {
            synchronized (this) {
                // check again to make sure it has not been created by another thread
                if (queueThread == null || !queueThread.isAlive()) {
                    logger.trace("Thread pool '{}' exhausted, queueing tasks now.", threadPoolName);
                    queueThread = createNewQueueThread();
                    queueThread.start();
                }
            }
        }
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        synchronized (semaphore) {
            semaphore.notify();
        }
    }

    /**
     * This implementation does not allow setting a custom handler.
     *
     * @throws UnsupportedOperationException if called.
     */
    @Override
    public void setRejectedExecutionHandler(RejectedExecutionHandler handler) {
        throw new UnsupportedOperationException();
    }

    @Override
    public BlockingQueue<Runnable> getQueue() {
        return taskQueue;
    }

    @Override
    public void execute(Runnable command) {
        // make sure that rejected tasks are executed before any new concurrently incoming tasks
        if (taskQueue.isEmpty()) {
            super.execute(command);
        } else {
            if (command == null) {
                throw new NullPointerException();
            }

            // ignore incoming tasks when the executor is shutdown
            if (!isShutdown()) {
                addToQueue(command);
            }
        }
    }

    private Thread createNewQueueThread() {
        Thread thread = getThreadFactory().newThread(new Runnable() {

            @Override
            public void run() {
                while (true) {
                    // check if some thread from the pool is idle
                    if (QueueingThreadPoolExecutor.this.getActiveCount() < QueueingThreadPoolExecutor.this
                            .getMaximumPoolSize()) {
                        try {
                            // keep waiting for max 2 seconds if further tasks are pushed to the queue
                            Runnable runnable = taskQueue.poll(2, TimeUnit.SECONDS);
                            if (runnable != null) {
                                logger.trace("Executing queued task of thread pool '{}'.", threadPoolName);
                                QueueingThreadPoolExecutor.super.execute(runnable);
                            } else {
                                break;
                            }
                        } catch (InterruptedException e) {
                        }
                    } else {
                        // let's wait for a thread to become available, but max. 1 second
                        try {
                            synchronized (semaphore) {
                                semaphore.wait(1000);
                            }
                        } catch (InterruptedException e) {
                        }
                    }
                }
                logger.trace("Queue for thread pool '{}' fully processed - terminating queue thread.", threadPoolName);
            }
        });
        thread.setName(threadPoolName + "-queue");
        return thread;
    }

    /**
     * This is the internally used thread factory, which creates non-daemon threads and assigns them a sequentially
     * indexed name.
     */
    private static class CommonThreadFactory implements ThreadFactory {

        protected final ThreadGroup group;
        protected final AtomicInteger threadNumber = new AtomicInteger(1);
        protected final String name;

        public CommonThreadFactory(String name) {
            this.name = name;
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r, name + "-" + threadNumber.getAndIncrement(), 0);
            if (t.isDaemon()) {
                t.setDaemon(false);
            }
            if (t.getPriority() != Thread.NORM_PRIORITY) {
                t.setPriority(Thread.NORM_PRIORITY);
            }

            return t;
        }
    }

    /**
     * This is the internally used rejection handler, which - instead of rejecting a task - puts it to the queue of the
     * pool.
     */
    private static class QueueingRejectionHandler extends ThreadPoolExecutor.DiscardPolicy {

        @Override
        public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) {
            if (!threadPoolExecutor.isShutdown()) {
                QueueingThreadPoolExecutor queueingThreadPoolExecutor = (QueueingThreadPoolExecutor) threadPoolExecutor;
                queueingThreadPoolExecutor.addToQueue(runnable);
            }
        }
    }
}