/*
 * 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.pulsar.client.api;

import java.io.Closeable;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.apache.pulsar.common.classification.InterfaceAudience;
import org.apache.pulsar.common.classification.InterfaceStability;

/**
 * A Reader can be used to scan through all the messages currently available in a topic.
 */
@InterfaceAudience.Public
@InterfaceStability.Stable
public interface Reader<T> extends Closeable {

    /**
     * @return the topic from which this reader is reading from
     */
    String getTopic();

    /**
     * Read the next message in the topic.
     *
     * <p>This method will block until a message is available.
     *
     * @return the next message
     * @throws PulsarClientException
     */
    Message<T> readNext() throws PulsarClientException;

    /**
     * Read the next message in the topic waiting for a maximum time.
     *
     * <p>Returns null if no message is received before the timeout.
     *
     * @return the next message(Could be null if none received in time)
     * @throws PulsarClientException
     */
    Message<T> readNext(int timeout, TimeUnit unit) throws PulsarClientException;

    /**
     * Read asynchronously the next message in the topic.
     *
     * <p>{@code readNextAsync()} should be called subsequently once returned {@code CompletableFuture} gets complete
     * with received message. Else it creates <i> backlog of receive requests </i> in the application.
     *
     * <p>The returned future can be cancelled before completion by calling {@code .cancel(false)}
     * ({@link CompletableFuture#cancel(boolean)}) to remove it from the the backlog of receive requests. Another
     * choice for ensuring a proper clean up of the returned future is to use the CompletableFuture.orTimeout method
     * which is available on JDK9+. That would remove it from the backlog of receive requests if receiving exceeds
     * the timeout.
     *
     * @return a future that will yield a message (when it's available) or {@link PulsarClientException} if the reader
     *         is already closed.
     */
    CompletableFuture<Message<T>> readNextAsync();

    /**
     * Asynchronously close the reader and stop the broker to push more messages.
     *
     * @return a future that can be used to track the completion of the operation
     */
    CompletableFuture<Void> closeAsync();

    /**
     * Return true if the topic was terminated and this reader has reached the end of the topic.
     *
     * <p>Note that this only applies to a "terminated" topic (where the topic is "sealed" and no
     * more messages can be published) and not just that the reader is simply caught up with
     * the publishers. Use {@link #hasMessageAvailable()} to check for for that.
     */
    boolean hasReachedEndOfTopic();

    /**
     * Check if there is any message available to read from the current position.
     *
     * <p>This check can be used by an application to scan through a topic and stop
     * when the reader reaches the current last published message. For example:
     *
     * <pre>{@code
     * while (reader.hasMessageAvailable()) {
     *     Message<String> msg = reader.readNext();
     *     // Do something
     * }
     *
     * // Done reading
     * }</pre>
     *
     * <p>Note that this call might be blocking (see {@link #hasMessageAvailableAsync()} for async version) and
     * that even if this call returns true, that will not guarantee that a subsequent call to {@link #readNext()}
     * will not block.
     *
     * @return true if the are messages available to be read, false otherwise
     * @throws PulsarClientException if there was any error in the operation
     */
    boolean hasMessageAvailable() throws PulsarClientException;

    /**
     * Asynchronously check if there is any message available to read from the current position.
     *
     * <p>This check can be used by an application to scan through a topic and stop when the reader reaches the current
     * last published message.
     *
     * @return a future that will yield true if the are messages available to be read, false otherwise, or a
     *         {@link PulsarClientException} if there was any error in the operation
     */
    CompletableFuture<Boolean> hasMessageAvailableAsync();

    /**
     * @return Whether the reader is connected to the broker
     */
    boolean isConnected();

    /**
     * Reset the subscription associated with this reader to a specific message id.
     *
     * <p>The message id can either be a specific message or represent the first or last messages in the topic.
     * <ul>
     * <li><code>MessageId.earliest</code> : Reset the reader on the earliest message available in the topic
     * <li><code>MessageId.latest</code> : Reset the reader on the latest message in the topic
     * </ul>
     *
     * <p>Note: this operation can only be done on non-partitioned topics. For these, one can rather perform
     * the seek() on the individual partitions.
     *
     * @param messageId the message id where to reposition the reader
     */
    void seek(MessageId messageId) throws PulsarClientException;

    /**
     * Reset the subscription associated with this reader to a specific message publish time.
     *
     * <p>Note: this operation can only be done on non-partitioned topics. For these, one can rather perform
     * the seek() on the individual partitions.
     *
     * @param timestamp the message publish time where to reposition the reader
     *                  The timestamp format should be Unix time in milliseconds.
     */
    void seek(long timestamp) throws PulsarClientException;

    /**
     * Reset the subscription associated with this consumer to a specific message ID or message publish time.
     * <p>
     * The Function input is topic+partition. It returns only timestamp or MessageId.
     * <p>
     * The return value is the seek position/timestamp of the current partition.
     * Exception is thrown if other object types are returned.
     * <p>
     * If returns null, the current partition will not do any processing.
     * Exception in a partition may affect other partitions.
     * @param function
     * @throws PulsarClientException
     */
    void seek(Function<String, Object> function) throws PulsarClientException;

    /**
     * Reset the subscription associated with this consumer to a specific message ID
     * or message publish time asynchronously.
     * <p>
     * The Function input is topic+partition. It returns only timestamp or MessageId.
     * <p>
     * The return value is the seek position/timestamp of the current partition.
     * Exception is thrown if other object types are returned.
     * <p>
     * If returns null, the current partition will not do any processing.
     * Exception in a partition may affect other partitions.
     * @param function
     * @return
     */
    CompletableFuture<Void> seekAsync(Function<String, Object> function);

    /**
     * Reset the subscription associated with this reader to a specific message id.
     *
     * <p>The message id can either be a specific message or represent the first or last messages in the topic.
     * <ul>
     * <li><code>MessageId.earliest</code> : Reset the reader on the earliest message available in the topic
     * <li><code>MessageId.latest</code> : Reset the reader on the latest message in the topic
     * </ul>
     *
     * <p>Note: this operation can only be done on non-partitioned topics. For these, one can rather perform
     * the seek() on the individual partitions.
     *
     * @param messageId the message id where to position the reader
     * @return a future to track the completion of the seek operation
     */
    CompletableFuture<Void> seekAsync(MessageId messageId);

    /**
     * Reset the subscription associated with this reader to a specific message publish time.
     *
     * <p>Note: this operation can only be done on non-partitioned topics. For these, one can rather perform
     * the seek() on the individual partitions.
     *
     * @param timestamp
     *            the message publish time where to position the reader
     *            The timestamp format should be Unix time in milliseconds.
     * @return a future to track the completion of the seek operation
     */
    CompletableFuture<Void> seekAsync(long timestamp);

    /**
     * Get all the last message id of the topics the reader subscribed.
     *
     * @return the list of TopicMessageId instances of all the topics that the reader subscribed
     * @throws PulsarClientException if failed to get last message id.
     * @apiNote It's guaranteed that the owner topic of each TopicMessageId in the returned list is different from owner
     *   topics of other TopicMessageId instances
     */
    List<TopicMessageId> getLastMessageIds() throws PulsarClientException;

    /**
     * The asynchronous version of {@link Reader#getLastMessageIds()}.
     */
    CompletableFuture<List<TopicMessageId>> getLastMessageIdsAsync();
}
