// Copyright (c) 2007-Present Pivotal Software, Inc.  All rights reserved.
//
// This software, the RabbitMQ Java client library, is triple-licensed under the
// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2
// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see
// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2.  For the ASL,
// please see LICENSE-APACHE2.
//
// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
// either express or implied. See the LICENSE file for specific language governing
// rights and limitations of this software.
//
// If you have any questions regarding licensing, please contact us at
// info@rabbitmq.com.


package com.rabbitmq.client;

import com.rabbitmq.utility.Utility;

import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * Class which manages a request queue for a simple RPC-style service.
 * The class is agnostic about the format of RPC arguments / return values.
*/
public class RpcServer {
    /** Channel we are communicating on */
    private final Channel _channel;
    /** Queue to receive requests from */
    private final String _queueName;
    /** Boolean controlling the exit from the mainloop. */
    private volatile boolean _mainloopRunning = true;

    /** Consumer attached to our request queue */
    private RpcConsumer _consumer;

    /**
     * Creates an RpcServer listening on a temporary exclusive
     * autodelete queue.
     */
    public RpcServer(Channel channel)
        throws IOException
    {
        this(channel, null);
    }

    /**
     * If the passed-in queue name is null, creates a server-named
     * temporary exclusive autodelete queue to use; otherwise expects
     * the queue to have already been declared.
     */
    public RpcServer(Channel channel, String queueName)
        throws IOException
    {
        _channel = channel;
        if (queueName == null || queueName.equals("")) {
            _queueName = _channel.queueDeclare().getQueue();
        } else {
            _queueName = queueName;
        }
        _consumer = setupConsumer();
    }

    /**
     * Public API - cancels the consumer, thus deleting the queue, if
     * it was a temporary queue, and marks the RpcServer as closed.
     * @throws IOException if an error is encountered
     */
    public void close()
        throws IOException
    {
        if (_consumer != null) {
            _channel.basicCancel(_consumer.getConsumerTag());
            _consumer = null;
        }
        terminateMainloop();
    }

    /**
     * Registers a consumer on the reply queue.
     * @throws IOException if an error is encountered
     * @return the newly created and registered consumer
     */
    protected RpcConsumer setupConsumer()
        throws IOException
    {
        RpcConsumer consumer = new DefaultRpcConsumer(_channel);
        _channel.basicConsume(_queueName, consumer);
        return consumer;
    }

    /**
     * Public API - main server loop. Call this to begin processing
     * requests. Request processing will continue until the Channel
     * (or its underlying Connection) is shut down, or until
     * terminateMainloop() is called.
     *
     * Note that if the mainloop is blocked waiting for a request, the
     * termination flag is not checked until a request is received, so
     * a good time to call terminateMainloop() is during a request
     * handler.
     *
     * @return the exception that signalled the Channel shutdown, or null for orderly shutdown
     */
    public ShutdownSignalException mainloop()
        throws IOException
    {
        try {
            while (_mainloopRunning) {
                Delivery request;
                try {
                    request = _consumer.nextDelivery();
                } catch (InterruptedException ie) {
                    continue;
                }
                processRequest(request);
                _channel.basicAck(request.getEnvelope().getDeliveryTag(), false);
            }
            return null;
        } catch (ShutdownSignalException sse) {
            return sse;
        }
    }

    /**
     * Call this method to terminate the mainloop.
     *
     * Note that if the mainloop is blocked waiting for a request, the
     * termination flag is not checked until a request is received, so
     * a good time to call terminateMainloop() is during a request
     * handler.
     */
    public void terminateMainloop() {
        _mainloopRunning = false;
    }

    /**
     * Private API - Process a single request. Called from mainloop().
     */
    public void processRequest(Delivery request)
        throws IOException
    {
        AMQP.BasicProperties requestProperties = request.getProperties();
        String correlationId = requestProperties.getCorrelationId();
        String replyTo = requestProperties.getReplyTo();
        if (correlationId != null && replyTo != null)
        {
            AMQP.BasicProperties.Builder replyPropertiesBuilder
                = new AMQP.BasicProperties.Builder().correlationId(correlationId);
            AMQP.BasicProperties replyProperties = preprocessReplyProperties(request, replyPropertiesBuilder);
            byte[] replyBody = handleCall(request, replyProperties);
            replyProperties = postprocessReplyProperties(request, replyProperties.builder());
            _channel.basicPublish("", replyTo, replyProperties, replyBody);
        } else {
            handleCast(request);
        }
    }

    /**
     * Lowest-level response method. Calls
     * handleCall(AMQP.BasicProperties,byte[],AMQP.BasicProperties).
     */
    public byte[] handleCall(Delivery request,
                             AMQP.BasicProperties replyProperties)
    {
        return handleCall(request.getProperties(),
                          request.getBody(),
                          replyProperties);
    }

    /**
     * Mid-level response method. Calls
     * handleCall(byte[],AMQP.BasicProperties).
     */
    public byte[] handleCall(AMQP.BasicProperties requestProperties,
                             byte[] requestBody,
                             AMQP.BasicProperties replyProperties)
    {
        return handleCall(requestBody, replyProperties);
    }

    /**
     * High-level response method. Returns an empty response by
     * default - override this (or other handleCall and handleCast
     * methods) in subclasses.
     */
    public byte[] handleCall(byte[] requestBody,
                             AMQP.BasicProperties replyProperties)
    {
        return new byte[0];
    }

    /**
     * Gives a chance to set/modify reply properties before handling call.
     * Note the correlationId property is already set.
     * @param request the inbound message
     * @param builder the reply properties builder
     * @return the properties to pass in to the handling call
     */
    protected AMQP.BasicProperties preprocessReplyProperties(Delivery request, AMQP.BasicProperties.Builder builder) {
        return builder.build();
    }

    /**
     * Gives a chance to set/modify reply properties after the handling call
     * @param request the inbound message
     * @param builder the reply properties builder
     * @return the properties to pass in to the response message
     */
    protected AMQP.BasicProperties postprocessReplyProperties(Delivery request, AMQP.BasicProperties.Builder builder) {
        return builder.build();
    }

    /**
     * Lowest-level handler method. Calls
     * handleCast(AMQP.BasicProperties,byte[]).
     */
    public void handleCast(Delivery request)
    {
        handleCast(request.getProperties(), request.getBody());
    }

    /**
     * Mid-level handler method. Calls
     * handleCast(byte[]).
     */
    public void handleCast(AMQP.BasicProperties requestProperties, byte[] requestBody)
    {
        handleCast(requestBody);
    }

    /**
     * High-level handler method. Does nothing by default - override
     * this (or other handleCast and handleCast methods) in
     * subclasses.
     */
    public void handleCast(byte[] requestBody)
    {
        // Does nothing.
    }

    /**
     * Retrieve the channel.
     * @return the channel to which this server is connected
     */
    public Channel getChannel() {
        return _channel;
    }

    /**
     * Retrieve the queue name.
     * @return the queue which this server is consuming from
     */
    public String getQueueName() {
        return _queueName;
    }

    public interface RpcConsumer extends Consumer {

        Delivery nextDelivery() throws InterruptedException, ShutdownSignalException, ConsumerCancelledException;

        String getConsumerTag();

    }

    private static class DefaultRpcConsumer extends DefaultConsumer implements RpcConsumer {

        // Marker object used to signal the queue is in shutdown mode.
        // It is only there to wake up consumers. The canonical representation
        // of shutting down is the presence of _shutdown.
        // Invariant: This is never on _queue unless _shutdown != null.
        private static final Delivery POISON = new Delivery(null, null, null);
        private final BlockingQueue<Delivery> _queue;
        // When this is non-null the queue is in shutdown mode and nextDelivery should
        // throw a shutdown signal exception.
        private volatile ShutdownSignalException _shutdown;
        private volatile ConsumerCancelledException _cancelled;

        public DefaultRpcConsumer(Channel ch) {
            this(ch, new LinkedBlockingQueue<>());
        }

        public DefaultRpcConsumer(Channel ch, BlockingQueue<Delivery> q) {
            super(ch);
            this._queue = q;
        }

        @Override
        public Delivery nextDelivery() throws InterruptedException, ShutdownSignalException, ConsumerCancelledException {
            return handle(_queue.take());
        }

        @Override
        public void handleShutdownSignal(String consumerTag,
            ShutdownSignalException sig) {
            _shutdown = sig;
            _queue.add(POISON);
        }

        @Override
        public void handleCancel(String consumerTag) throws IOException {
            _cancelled = new ConsumerCancelledException();
            _queue.add(POISON);
        }

        @Override
        public void handleDelivery(String consumerTag,
            Envelope envelope,
            AMQP.BasicProperties properties,
            byte[] body)
            throws IOException {
            checkShutdown();
            this._queue.add(new Delivery(envelope, properties, body));
        }

        /**
         * Check if we are in shutdown mode and if so throw an exception.
         */
        private void checkShutdown() {
            if (_shutdown != null)
                throw Utility.fixStackTrace(_shutdown);
        }

        /**
         * If delivery is not POISON nor null, return it.
         * <p/>
         * If delivery, _shutdown and _cancelled are all null, return null.
         * <p/>
         * If delivery is POISON re-insert POISON into the queue and
         * throw an exception if POISONed for no reason.
         * <p/>
         * Otherwise, if we are in shutdown mode or cancelled,
         * throw a corresponding exception.
         */
        private Delivery handle(Delivery delivery) {
            if (delivery == POISON ||
                delivery == null && (_shutdown != null || _cancelled != null)) {
                if (delivery == POISON) {
                    _queue.add(POISON);
                    if (_shutdown == null && _cancelled == null) {
                        throw new IllegalStateException(
                            "POISON in queue, but null _shutdown and null _cancelled. " +
                                "This should never happen, please report as a BUG");
                    }
                }
                if (null != _shutdown)
                    throw Utility.fixStackTrace(_shutdown);
                if (null != _cancelled)
                    throw Utility.fixStackTrace(_cancelled);
            }
            return delivery;
        }
    }


}

