/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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 org.apache.camel.component.kestrel;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

import net.spy.memcached.MemcachedClient;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.ShutdownRunningTask;
import org.apache.camel.impl.DefaultConsumer;
import org.apache.camel.spi.ShutdownAware;

/**
 * A Camel consumer that polls a kestrel queue.
 */
public class KestrelConsumer extends DefaultConsumer implements ShutdownAware {
    private final KestrelEndpoint endpoint;
    private final MemcachedClient memcachedClient;
    private final BlockingQueue<Exchanger<?>> exchangerQueue = new LinkedBlockingQueue<>();
    private ExecutorService pollerExecutor;
    private ExecutorService handlerExecutor;
    private volatile boolean shutdownPending;
    private CountDownLatch shutdownLatch;
    private AtomicInteger pendingExchangeCount = new AtomicInteger(0);

    public KestrelConsumer(final KestrelEndpoint endpoint, Processor processor, final MemcachedClient memcachedClient) {
        super(endpoint, processor);
        this.endpoint = endpoint;
        this.memcachedClient = memcachedClient;
    }

    @Override
    protected void doStart() throws Exception {
        log.info("Starting consumer for {}", endpoint.getEndpointUri());

        int poolSize = endpoint.getConfiguration().getConcurrentConsumers();

        shutdownPending = false;

        if (poolSize > 1) {
            // We'll set the shutdown latch to poolSize + 1, since we'll also
            // wait for the poller thread when shutting down.
            shutdownLatch = new CountDownLatch(poolSize + 1);

            // Fire up the handler thread pool
            handlerExecutor = endpoint.getCamelContext().getExecutorServiceManager().newFixedThreadPool(this, "Handlers-" + endpoint.getEndpointUri(), poolSize);
            for (int k = 0; k < poolSize; ++k) {
                handlerExecutor.execute(new Handler());
            }
        } else {
            // Since we only have concurrentConsumers=1, we'll do the handling
            // inside the poller thread, so there will only be one thread to
            // wait for on this latch.
            shutdownLatch = new CountDownLatch(1);
        }

        // Fire up the single poller thread
        pollerExecutor = endpoint.getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this, "Poller-" + endpoint.getEndpointUri());
        pollerExecutor.submit(new Poller(poolSize > 1));

        super.doStart();
    }

    @Override
    protected void doStop() throws Exception {
        log.info("Stopping consumer for {}", endpoint.getEndpointUri());

        if (pollerExecutor != null) {
            endpoint.getCamelContext().getExecutorServiceManager().shutdown(pollerExecutor);
            pollerExecutor = null;
        }
        if (handlerExecutor != null) {
            endpoint.getCamelContext().getExecutorServiceManager().shutdown(handlerExecutor);
            handlerExecutor = null;
        }

        super.doStop();
    }

    public boolean deferShutdown(ShutdownRunningTask shutdownRunningTask) {
        return false;
    }

    public int getPendingExchangesSize() {
        return pendingExchangeCount.get();
    }

    @Override
    public void prepareShutdown(boolean suspendOnly, boolean forced) {
        // Signal to our threads that shutdown is happening
        shutdownPending = true;

        if (log.isDebugEnabled()) {
            log.debug("Preparing to shutdown, waiting for {} threads to complete.", shutdownLatch.getCount());
        }

        // Wait for all threads to end
        try {
            shutdownLatch.await();
        } catch (InterruptedException e) {
            // ignore
        }
    }

    /**
     * This single thread is responsible for reading objects from kestrel and
     * dispatching them to the handler threads.  The catch is that we don't
     * want to poll kestrel until we know we have a handler thread available
     * and waiting to handle whatever comes up.  So the way we deal with that
     * is...each handler thread has an exchanger used to "receive" objects
     * from the kestrel reader thread.  When a handler thread is ready for
     * work, it simply puts its exchanger in the queue.  The kestrel reader
     * thread takes an exchanger from the queue (which will block until one
     * is there), and *then* it can poll kestrel.  Once an object is received
     * from kestrel, it gets exchanged with the handler thread, which can
     * take the object and process it.  Repeat...
     */
    @SuppressWarnings("unchecked")
    private final class Poller implements Runnable {
        private boolean concurrent;

        private Poller(boolean concurrent) {
            this.concurrent = concurrent;
        }

        public void run() {
            log.trace("Kestrel poller is running");

            // Construct the target key that we'll be requesting from kestrel.
            // Include the /t=... wait time as applicable.
            String target;
            if (endpoint.getConfiguration().getWaitTimeMs() > 0) {
                target = endpoint.getQueue() + "/t=" + endpoint.getConfiguration().getWaitTimeMs();
            } else {
                target = endpoint.getQueue();
            }

            @SuppressWarnings("rawtypes")
            Exchanger exchanger = null;
            while (isRunAllowed() && !shutdownPending) {
                if (concurrent) {
                    // Wait until an exchanger is available, indicating that a
                    // handler thread is ready to handle the next request.
                    // Don't read from kestrel until we know a handler is ready.
                    try {
                        exchanger = exchangerQueue.take();
                    } catch (InterruptedException e) {
                        if (log.isDebugEnabled()) {
                            log.debug("Interrupted, are we stopping? {}", isStopping() || isStopped());
                        }
                        continue;
                    }

                    // We have the exchanger, so there's a handler thread ready
                    // to handle whatever we may read...so read the next object
                    // from the queue.
                }

                // Poll kestrel until we get an object back
                Object value = null;
                while (isRunAllowed() && !shutdownPending) {
                    log.trace("Polling {}", target);
                    try {
                        value = memcachedClient.get(target);
                        if (value != null) {
                            break;
                        }
                    } catch (Exception e) {
                        if (isRunAllowed() && !shutdownPending) {
                            getExceptionHandler().handleException("Failed to get object from kestrel", e);
                        }
                    }

                    // We didn't get a value back from kestrel
                    if (isRunAllowed() && !shutdownPending) {
                        if (endpoint.getConfiguration().getWaitTimeMs() > 0) {
                            // Kestrel did the blocking for us
                        } else {
                            // We're doing non-blocking get, so in between we
                            // should at least sleep some short period of time
                            // so this loop doesn't go nuts so tightly.
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException ignored) {
                            }
                        }
                    }
                }

                log.trace("Got object from {}", target);

                if (concurrent) {
                    // Pass the object to the handler thread via the exchanger.
                    // The handler will take it from there.
                    try {
                        exchanger.exchange(value);
                    } catch (InterruptedException e) {
                        if (log.isDebugEnabled()) {
                            log.debug("Interrupted, are we stopping? {}", isStopping() || isStopped());
                        }
                        continue;
                    }
                } else {
                    // We're non-concurrent, so handle it right here
                    pendingExchangeCount.incrementAndGet();
                    try {
                        // Create the exchange and let camel process/route it
                        Exchange exchange = null;
                        try {
                            exchange = endpoint.createExchange();
                            exchange.getIn().setBody(value);
                            getProcessor().process(exchange);
                        } catch (Exception e) {
                            if (exchange != null) {
                                getExceptionHandler().handleException("Error processing exchange", exchange, e);
                            } else {
                                getExceptionHandler().handleException(e);
                            }
                        }
                    } finally {
                        // Decrement our pending exchange counter
                        pendingExchangeCount.decrementAndGet();
                    }
                }
            }
            log.trace("Finished polling {}", target);

            // Decrement the shutdown countdown latch
            shutdownLatch.countDown();
        }
    }

    private final class Handler implements Runnable {
        private Exchanger<Handler> exchanger = new Exchanger<>();

        public void run() {
            if (log.isTraceEnabled()) {
                log.trace("{} is starting", Thread.currentThread().getName());
            }

            while (isRunAllowed() && !shutdownPending) {
                // First things first, add our exchanger to the queue,
                // indicating that we're ready for a hand-off of work
                try {
                    exchangerQueue.put(exchanger);
                } catch (InterruptedException e) {
                    if (log.isDebugEnabled()) {
                        log.debug("Interrupted, are we stopping? {}", isStopping() || isStopped());
                    }
                    continue;
                }

                // Optimistically increment our internal pending exchange
                // counter, anticipating getting a value back from the exchanger
                pendingExchangeCount.incrementAndGet();
                try {
                    // Now wait for an object to come through the exchanger
                    Object value;
                    try {
                        value = exchanger.exchange(this);
                    } catch (InterruptedException e) {
                        if (log.isDebugEnabled()) {
                            log.debug("Interrupted, are we stopping? {}", isStopping() || isStopped());
                        }
                        continue;
                    }

                    log.trace("Got a value from the exchanger");

                    // Create the exchange and let camel process/route it
                    Exchange exchange = null;
                    try {
                        exchange = endpoint.createExchange();
                        exchange.getIn().setBody(value);
                        getProcessor().process(exchange);
                    } catch (Exception e) {
                        if (exchange != null) {
                            getExceptionHandler().handleException("Error processing exchange", exchange, e);
                        } else {
                            getExceptionHandler().handleException(e);
                        }
                    }
                } finally {
                    // Decrement our pending exchange counter
                    pendingExchangeCount.decrementAndGet();
                }
            }

            // Decrement the shutdown countdown latch
            shutdownLatch.countDown();

            if (log.isTraceEnabled()) {
                log.trace("{} is finished", Thread.currentThread().getName());
            }
        }
    }
}
