package bo.gotthardt.queue.rabbitmq;

import bo.gotthardt.queue.MessageQueue;
import bo.gotthardt.queue.MessageQueueException;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.GetResponse;
import com.rabbitmq.client.MessageProperties;
import io.dropwizard.jackson.Jackson;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.io.Closeable;
import java.io.IOException;
import java.util.function.Function;

/**
 * RabbitMQ implementation of {@link bo.gotthardt.queue.MessageQueue}.
 *
 * To instantiate, see {@link bo.gotthardt.queue.rabbitmq.RabbitMQBundle#getQueue(String, Class)}.
 *
 * @author Bo Gotthardt
 */
@Slf4j
class RabbitMQMessageQueue<T> implements MessageQueue<T> {
    private static final ObjectMapper MAPPER = Jackson.newObjectMapper();

    private final Channel channel;
    @Getter
    private final String name;
    private final Class<T> type;
    private final MetricRegistry metrics;
    private Meter publish;

    RabbitMQMessageQueue(Channel channel, String name, Class<T> type, MetricRegistry metrics) {
        this.channel = channel;
        this.name = name;
        this.type = type;
        this.metrics = metrics;
        this.publish = metrics.meter(MetricRegistry.name("queue", type.getSimpleName(), name, "publish"));
        try {
            channel.queueDeclare(name, true, false, false, null);
        } catch (IOException e) {
            throw new MessageQueueException("Unable to declare queue.", e);
        }
    }

    @Override
    public void publish(T message) {
        try {
            channel.basicPublish("", name, MessageProperties.PERSISTENT_TEXT_PLAIN, MAPPER.writeValueAsBytes(message));

            publish.mark();
            if (log.isTraceEnabled()) {
                log.trace("Published to '{}' with data '{}'.", name, MAPPER.writeValueAsString(message));
            }
        } catch (IOException e) {
            throw new MessageQueueException("Unable to publish to queue.", e);
        }
    }

    @Override
    public Closeable consume(Function<T, Void> processor) {
        Consumer consumer = new FunctionConsumer<>(channel, processor, type, name, metrics);

        try {
            String tag = channel.basicConsume(name, false, consumer);
            log.info("Set up consumer '{}' for queue '{}'.", tag, name);


            return () -> channel.basicCancel(tag);
        } catch (IOException e) {
            throw new MessageQueueException("Unable to set up consumer.", e);
        }
    }

    @Override
    public T consumeNext() {
        try {
            GetResponse response = channel.basicGet(name, true);
            return MAPPER.readValue(response.getBody(), type);
        } catch (IOException e) {
            throw new MessageQueueException("Unable to consume.", e);
        }
    }
}