/**
 *  Class describing a (mid-level) AMQP channel implementation
 *
 *  @copyright 2014 - 2023 Copernica BV
 */

/**
 *  Include guard
 */
#pragma once

/**
 *  Set up namespace
 */
namespace AMQP {

/**
 *  Class definition
 */
class Channel
{
private:
    /**
     *  The implementation for the channel
     *  @var    std::shared_ptr<ChannelImpl>
     */
    std::shared_ptr<ChannelImpl> _implementation;

public:
    /**
     *  Construct a channel object
     * 
     *  The passed in connection pointer must remain valid for the 
     *  lifetime of the channel. Watch out: this method throws an error
     *  if the channel could not be constructed (for example because the
     *  max number of AMQP channels has been reached)
     * 
     *  @param  connection
     *  @throws std::runtime_error
     */
    Channel(Connection *connection);
    
    /**
     *  Copy'ing of channel objects is not supported
     *  @param  channel
     */
    Channel(const Channel &channel) = delete;

    /**
     *  But movement _is_ allowed
     *  @param  channel
     */
    Channel(Channel &&channel) : _implementation(std::move(channel._implementation)) {}

    /**
     *  Destructor
     */
    virtual ~Channel() 
    {
        // close the channel (this will eventually destruct the channel)
        // note that the channel may be in an invalid state in case it was moved, hence the "if"
        if (_implementation) _implementation->close();
    }

    /**
     *  No assignments of other channels
     *  @param  channel
     *  @return Channel
     */
    Channel &operator=(const Channel &channel) = delete;

    /**
     *  Callback that is called when the channel was succesfully created.
     *
     *  Only one callback can be registered. Calling this function multiple
     *  times will remove the old callback.
     *
     *  @param  callback    the callback to execute
     */         
    inline void onReady(const SuccessCallback& callback) { return onReady(SuccessCallback(callback)); }
    void onReady(SuccessCallback&& callback)
    {
        _implementation->onReady(std::move(callback));
    }

    /**
     *  Callback that is called when an error occurs.
     *
     *  Only one error callback can be registered. Calling this function
     *  multiple times will remove the old callback.
     *
     *  @param  callback    the callback to execute
     */
    inline void onError(const ErrorCallback& callback) { return onError(ErrorCallback(callback)); }
    void onError(ErrorCallback&& callback)
    {
        _implementation->onError(std::move(callback));
    }

    /**
     *  Pause deliveries on a channel
     *
     *  This will stop all incoming messages
     *
     *  Note that this function does *not* work using RabbitMQ. For more info
     *  @see https://www.rabbitmq.com/specification.html#method-status-channel.flow
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &pause()
    {
        return _implementation->pause();
    }

    /**
     *  Resume a paused channel
     *
     *  This will resume incoming messages
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &resume()
    {
        return _implementation->resume();
    }

    /**
     *  Is the channel ready / has it passed the initial handshake?
     *  @return bool
     */
    bool ready() const
    {
        return _implementation->ready();
    }

    /**
     *  Is the channel usable / not yet closed?
     *  @return bool
     */
    bool usable() const
    {
        return _implementation->usable();
    }
    
    /**
     *  Is the channel connected?
     *  This method is deprecated: use Channel::usable()
     *  @return bool
     *  @deprecated
     */
    bool connected() const
    {
        return usable();
    }

    /**
     *  Put channel in a confirm mode (RabbitMQ specific)
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    DeferredConfirm &confirmSelect()
    {
        return _implementation->confirmSelect();
    }

    /**
     *  Start a transaction
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &startTransaction()
    {
        return _implementation->startTransaction();
    }

    /**
     *  Commit the current transaction
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &commitTransaction()
    {
        return _implementation->commitTransaction();
    }

    /**
     *  Rollback the current transaction
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &rollbackTransaction()
    {
        return _implementation->rollbackTransaction();
    }

    /**
     *  Declare an exchange
     *
     *  If an empty name is supplied, a name will be assigned by the server.
     *
     *  The following flags can be used for the exchange:
     *
     *      -   durable     exchange survives a broker restart
     *      -   autodelete  exchange is automatically removed when all connected queues are removed
     *      -   passive     only check if the exchange exist
     *      -   internal    create an internal exchange
     *
     *  @param  name        name of the exchange
     *  @param  type        exchange type
     *  @param  flags       exchange flags
     *  @param  arguments   additional arguments
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &declareExchange(const std::string_view &name, ExchangeType type, int flags, const Table &arguments) { return _implementation->declareExchange(name, type, flags, arguments); }
    Deferred &declareExchange(const std::string_view &name, ExchangeType type, const Table &arguments) { return _implementation->declareExchange(name, type, 0, arguments); }
    Deferred &declareExchange(const std::string_view &name, ExchangeType type = fanout, int flags = 0) { return _implementation->declareExchange(name, type, flags, Table()); }
    Deferred &declareExchange(ExchangeType type, int flags, const Table &arguments) { return _implementation->declareExchange(std::string_view(), type, flags, arguments); }
    Deferred &declareExchange(ExchangeType type, const Table &arguments) { return _implementation->declareExchange(std::string_view(), type, 0, arguments); }
    Deferred &declareExchange(ExchangeType type = fanout, int flags = 0) { return _implementation->declareExchange(std::string_view(), type, flags, Table()); }

    /**
     *  Remove an exchange
     *
     *  The following flags can be used for the exchange:
     *
     *      -   ifunused    only delete if no queues are connected

     *  @param  name        name of the exchange to remove
     *  @param  flags       optional flags
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &removeExchange(const std::string_view &name, int flags = 0) { return _implementation->removeExchange(name, flags); }

    /**
     *  Bind two exchanges to each other
     *
     *  @param  source      the source exchange
     *  @param  target      the target exchange
     *  @param  routingkey  the routing key
     *  @param  arguments   additional bind arguments
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &bindExchange(const std::string_view &source, const std::string_view &target, const std::string_view &routingkey, const Table &arguments) { return _implementation->bindExchange(source, target, routingkey, arguments); }
    Deferred &bindExchange(const std::string_view &source, const std::string_view &target, const std::string_view &routingkey) { return _implementation->bindExchange(source, target, routingkey, Table()); }

    /**
     *  Unbind two exchanges from one another
     *
     *  @param  target      the target exchange
     *  @param  source      the source exchange
     *  @param  routingkey  the routing key
     *  @param  arguments   additional unbind arguments
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &unbindExchange(const std::string_view &target, const std::string_view &source, const std::string_view &routingkey, const Table &arguments) { return _implementation->unbindExchange(target, source, routingkey, arguments); }
    Deferred &unbindExchange(const std::string_view &target, const std::string_view &source, const std::string_view &routingkey) { return _implementation->unbindExchange(target, source, routingkey, Table()); }

    /**
     *  Declare a queue
     *
     *  If you do not supply a name, a name will be assigned by the server.
     *
     *  The flags can be a combination of the following values:
     *
     *      -   durable     queue survives a broker restart
     *      -   autodelete  queue is automatically removed when all connected consumers are gone
     *      -   passive     only check if the queue exist
     *      -   exclusive   the queue only exists for this connection, and is automatically removed when connection is gone
     *
     *  @param  name        name of the queue
     *  @param  flags       combination of flags
     *  @param  arguments   optional arguments
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     *
     *  The onSuccess() callback that you can install should have the following signature:
     *
     *      void myCallback(const std::string &name, uint32_t messageCount, uint32_t consumerCount);
     *
     *  For example: channel.declareQueue("myqueue").onSuccess([](const std::string &name, uint32_t messageCount, uint32_t consumerCount) {
     *
     *      std::cout << "Queue '" << name << "' has been declared with " << messageCount << " messages and " << consumerCount << " consumers" << std::endl;
     *
     *  });
     */
    DeferredQueue &declareQueue(const std::string_view &name, int flags, const Table &arguments) { return _implementation->declareQueue(name, flags, arguments); }
    DeferredQueue &declareQueue(const std::string_view &name, const Table &arguments) { return _implementation->declareQueue(name, 0, arguments); }
    DeferredQueue &declareQueue(const std::string_view &name, int flags = 0) { return _implementation->declareQueue(name, flags, Table()); }
    DeferredQueue &declareQueue(int flags, const Table &arguments) { return _implementation->declareQueue(std::string_view(), flags, arguments); }
    DeferredQueue &declareQueue(const Table &arguments) { return _implementation->declareQueue(std::string_view(), 0, arguments); }
    DeferredQueue &declareQueue(int flags = 0) { return _implementation->declareQueue(std::string_view(), flags, Table()); }

    /**
     *  Bind a queue to an exchange
     *
     *  @param  exchange    the source exchange
     *  @param  queue       the target queue
     *  @param  routingkey  the routing key
     *  @param  arguments   additional bind arguments
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &bindQueue(const std::string_view &exchange, const std::string_view &queue, const std::string_view &routingkey, const Table &arguments) { return _implementation->bindQueue(exchange, queue, routingkey, arguments); }
    Deferred &bindQueue(const std::string_view &exchange, const std::string_view &queue, const std::string_view &routingkey) { return _implementation->bindQueue(exchange, queue, routingkey, Table()); }

    /**
     *  Unbind a queue from an exchange
     *  @param  exchange    the source exchange
     *  @param  queue       the target queue
     *  @param  routingkey  the routing key
     *  @param  arguments   additional bind arguments
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &unbindQueue(const std::string_view &exchange, const std::string_view &queue, const std::string_view &routingkey, const Table &arguments) {  return _implementation->unbindQueue(exchange, queue, routingkey, arguments); }
    Deferred &unbindQueue(const std::string_view &exchange, const std::string_view &queue, const std::string_view &routingkey) { return _implementation->unbindQueue(exchange, queue, routingkey, Table()); }

    /**
     *  Purge a queue
     *
     *  @param  name        name of the queue
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     *
     *  The onSuccess() callback that you can install should have the following signature:
     *
     *      void myCallback(uint32_t messageCount);
     *
     *  For example: channel.purgeQueue("myqueue").onSuccess([](uint32_t messageCount) {
     *
     *      std::cout << "Queue purged, all " << messageCount << " messages removed" << std::endl;
     *
     *  });
     */
    DeferredDelete &purgeQueue(const std::string_view &name){ return _implementation->purgeQueue(name); }

    /**
     *  Remove a queue
     *
     *  The following flags can be used for the exchange:
     *
     *      -   ifunused    only delete if no consumers are connected
     *      -   ifempty     only delete if the queue is empty
     *
     *  @param  name        name of the queue to remove
     *  @param  flags       optional flags
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     *
     *  The onSuccess() callback that you can install should have the following signature:
     *
     *      void myCallback(uint32_t messageCount);
     *
     *  For example: channel.removeQueue("myqueue").onSuccess([](uint32_t messageCount) {
     *
     *      std::cout << "Queue deleted, along with " << messageCount << " messages" << std::endl;
     *
     *  });
     */
    DeferredDelete &removeQueue(const std::string_view &name, int flags = 0) { return _implementation->removeQueue(name, flags); }

    /**
     *  Publish a message to an exchange
     * 
     *  You have to supply the name of an exchange and a routing key. RabbitMQ will then try
     *  to send the message to one or more queues. With the optional flags parameter you can
     *  specify what should happen if the message could not be routed to a queue. By default,
     *  unroutable message are silently discarded.
     * 
     *  If you set the 'mandatory' and/or 'immediate' flag, messages that could not be handled 
     *  are returned to the application. Make sure that you have called the recall()-method and
     *  have set up all appropriate handlers to process these returned messages before you start
     *  publishing.
     * 
     *  The following flags can be supplied:
     * 
     *      -   mandatory   If set, server returns messages that are not sent to a queue
     *      -   immediate   If set, server returns messages that can not immediately be forwarded to a consumer. 
     * 
     *  @param  exchange    the exchange to publish to
     *  @param  routingkey  the routing key
     *  @param  envelope    the full envelope to send
     *  @param  message     the message to send
     *  @param  size        size of the message
     *  @param  flags       optional flags
     */
    bool publish(const std::string_view &exchange, const std::string_view &routingKey, const Envelope &envelope, int flags = 0) { return _implementation->publish(exchange, routingKey, envelope, flags); }
    bool publish(const std::string_view &exchange, const std::string_view &routingKey, const std::string &message, int flags = 0) { return _implementation->publish(exchange, routingKey, Envelope(message.data(), message.size()), flags); }
    bool publish(const std::string_view &exchange, const std::string_view &routingKey, const char *message, size_t size, int flags = 0) { return _implementation->publish(exchange, routingKey, Envelope(message, size), flags); }
    bool publish(const std::string_view &exchange, const std::string_view &routingKey, const char *message, int flags = 0) { return _implementation->publish(exchange, routingKey, Envelope(message, strlen(message)), flags); }

    /**
     *  Set the Quality of Service (QOS) for this channel
     *
     *  When you consume messages, every single message needs to be ack'ed to inform
     *  the RabbitMQ server that is has been received. The Qos setting specifies the
     *  number of unacked messages that may exist in the client application. The server
     *  stops delivering more messages if the number of unack'ed messages has reached
     *  the prefetchCount
     *
     *  @param  prefetchCount       maximum number of messages to prefetch
     *  @param  global              share counter between all consumers on the same channel
     *  @return bool                whether the Qos frame is sent.
     */
    Deferred &setQos(uint16_t prefetchCount, bool global = false)
    {
        return _implementation->setQos(prefetchCount, global);
    }

    /**
     *  Tell the RabbitMQ server that we're ready to consume messages
     *
     *  After this method is called, RabbitMQ starts delivering messages to the client
     *  application. The consume tag is a string identifier that you can use to identify
     *  the consumer if you later want to stop it with with a channel::cancel() call. 
     *  If you do not specify a consumer tag, the server will assign one for you.
     *
     *  The following flags are supported:
     *
     *      -   nolocal             if set, messages published on this channel are not also consumed
     *      -   noack               if set, consumed messages do not have to be acked, this happens automatically
     *      -   exclusive           request exclusive access, only this consumer can access the queue
     *
     *  @param  queue               the queue from which you want to consume
     *  @param  tag                 a consumer tag that will be associated with this consume operation
     *  @param  flags               additional flags
     *  @param  arguments           additional arguments
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     *
     *  The onSuccess() callback that you can install should have the following signature:
     *
     *      void myCallback(const std::string_view& tag);
     *
     *  For example: channel.consume("myqueue").onSuccess([](const std::string_view& tag) {
     *
     *      std::cout << "Started consuming under tag " << tag << std::endl;
     *
     *  });
     */
    DeferredConsumer &consume(const std::string_view &queue, const std::string_view &tag, int flags, const Table &arguments) { return _implementation->consume(queue, tag, flags, arguments); }
    DeferredConsumer &consume(const std::string_view &queue, const std::string_view &tag, int flags = 0) { return _implementation->consume(queue, tag, flags, Table()); }
    DeferredConsumer &consume(const std::string_view &queue, const std::string_view &tag, const Table &arguments) { return _implementation->consume(queue, tag, 0, arguments); }
    DeferredConsumer &consume(const std::string_view &queue, int flags, const Table &arguments) { return _implementation->consume(queue, std::string_view(), flags, arguments); }
    DeferredConsumer &consume(const std::string_view &queue, int flags = 0) { return _implementation->consume(queue, std::string_view(), flags, Table()); }
    DeferredConsumer &consume(const std::string_view &queue, const Table &arguments) { return _implementation->consume(queue, std::string_view(), 0, arguments); }

    /**
     *  Tell the messages that you are ready to recall/take back messages that messages thar are unroutable.
     * 
     *  When you use the publish() method in combination with the 'immediate' or 'mandatory' flag, rabbitmq
     *  sends back unroutable messages. With this recall() method you can install a sort of pseudo-consumer
     *  that defines how such returned-messages are processed.
     * 
     *  Watch out: when you call this method more than once, you always get access to the same object. You
     *  can thus not install multiple callbacks for the same event.
     */
    DeferredRecall &recall() { return _implementation->recall(); }

    /**
     *  Cancel a running consume call
     *
     *  If you want to stop a running consumer, you can use this method with the consumer tag
     *
     *  @param  tag                 the consumer tag
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     *
     *  The onSuccess() callback that you can install should have the following signature:
     *
     *      void myCallback(const std::string& tag);
     *
     *  For example: channel.cancel("myqueue").onSuccess([](const std::string& tag) {
     *
     *      std::cout << "Stopped consuming under tag " << tag << std::endl;
     *
     *  });
     */
    DeferredCancel &cancel(const std::string_view &tag) { return _implementation->cancel(tag); }

    /**
     *  Retrieve a single message from RabbitMQ
     * 
     *  When you call this method, you can get one single message from the queue (or none
     *  at all if the queue is empty). The deferred object that is returned, should be used
     *  to install a onEmpty() and onSuccess() callback function that will be called
     *  when the message is consumed and/or when the message could not be consumed.
     * 
     *  The following flags are supported:
     * 
     *      -   noack               if set, consumed messages do not have to be acked, this happens automatically
     * 
     *  @param  queue               name of the queue to consume from
     *  @param  flags               optional flags
     * 
     *  The object returns a deferred handler. Callbacks can be installed 
     *  using onSuccess(), onEmpty(), onError() and onFinalize() methods.
     * 
     *  The onSuccess() callback has the following signature:
     * 
     *      void myCallback(const Message &message, uint64_t deliveryTag, bool redelivered);
     * 
     *  For example: channel.get("myqueue").onSuccess([](const Message &message, uint64_t deliveryTag, bool redelivered) {
     * 
     *      std::cout << "Message fetched" << std::endl;
     * 
     *  }).onEmpty([]() {
     * 
     *      std::cout << "Queue is empty" << std::endl;
     * 
     *  });
     */
    DeferredGet &get(const std::string_view &queue, int flags = 0) { return _implementation->get(queue, flags); }

    /**
     *  Acknoldge a received message
     *
     *  When a message is received in the DeferredConsumer::onReceived() method,
     *  you must acknowledge it so that RabbitMQ removes it from the queue (unless
     *  you are consuming with the noack option). This method can be used for
     *  this acknowledging.
     *
     *  The following flags are supported:
     *
     *      -   multiple            acknowledge multiple messages: all un-acked messages that were earlier delivered are acknowledged too
     *
     *  @param  deliveryTag         the unique delivery tag of the message
     *  @param  flags               optional flags
     *  @return bool
     */
    bool ack(uint64_t deliveryTag, int flags=0) { return _implementation->ack(deliveryTag, flags); }

    /**
     *  Reject or nack a message
     *
     *  When a message was received in the DeferredConsumer::onReceived() method,
     *  and you don't want to acknowledge it, you can also choose to reject it by
     *  calling this reject method.
     *
     *  The following flags are supported:
     *
     *      -   multiple            reject multiple messages: all un-acked messages that were earlier delivered are unacked too
     *      -   requeue             if set, the message is put back in the queue, otherwise it is dead-lettered/removed
     *
     *  @param  deliveryTag         the unique delivery tag of the message
     *  @param  flags               optional flags
     *  @return bool
     */
    bool reject(uint64_t deliveryTag, int flags=0) { return _implementation->reject(deliveryTag, flags); }

    /**
     *  Recover all messages that were not yet acked
     *
     *  This method asks the server to redeliver all unacknowledged messages on a specified
     *  channel. Zero or more messages may be redelivered.
     *
     *  The following flags are supported:
     *
     *      -   requeue             if set, the server will requeue the messages, so the could also end up with at different consumer
     *
     *  @param  flags
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &recover(int flags = 0) { return _implementation->recover(flags); }

    /**
     *  Close the current channel
     *
     *  This function returns a deferred handler. Callbacks can be installed
     *  using onSuccess(), onError() and onFinalize() methods.
     */
    Deferred &close() { return _implementation->close(); }

    /**
     *  Get the channel we're working on
     *  @return uint16_t
     */
    uint16_t id() const
    {
        return _implementation->id();
    }

    /**
     *  Some internal classes may touch our implementation
     */
    friend class Tagger;
};

/**
 *  end namespace
 */
}

