/*
 * #%L
 * ReceiveRunnable.java - mongodb-async-driver - Allanbank Consulting, Inc.
 * %%
 * Copyright (C) 2011 - 2014 Allanbank Consulting, Inc.
 * %%
 * Licensed 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.
 * #L%
 */

package com.allanbank.mongodb.client.transport.bio;

import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.StreamCorruptedException;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.util.logging.Level;

import com.allanbank.mongodb.MongoClientConfiguration;
import com.allanbank.mongodb.MongoDbException;
import com.allanbank.mongodb.bson.io.BsonInputStream;
import com.allanbank.mongodb.client.Message;
import com.allanbank.mongodb.client.Operation;
import com.allanbank.mongodb.client.callback.Receiver;
import com.allanbank.mongodb.client.message.Delete;
import com.allanbank.mongodb.client.message.GetMore;
import com.allanbank.mongodb.client.message.Header;
import com.allanbank.mongodb.client.message.Insert;
import com.allanbank.mongodb.client.message.KillCursors;
import com.allanbank.mongodb.client.message.Query;
import com.allanbank.mongodb.client.message.Reply;
import com.allanbank.mongodb.client.message.Update;
import com.allanbank.mongodb.client.transport.TransportResponseListener;
import com.allanbank.mongodb.error.ConnectionLostException;
import com.allanbank.mongodb.util.IOUtils;
import com.allanbank.mongodb.util.log.Log;
import com.allanbank.mongodb.util.log.LogFactory;

/**
 * Runnable to receive messages from an {@link AbstractSocketTransport}.
 *
 * @api.no This class is <b>NOT</b> part of the drivers API. This class may be
 *         mutated in incompatible ways between any two releases of the driver.
 * @copyright 2014, Allanbank Consulting, Inc., All Rights Reserved
 */
public class ReceiveRunnable
        implements Runnable, Receiver {
    /** The logger for the receive thread. */
    private static final Log LOG = LogFactory.getLog(ReceiveRunnable.class);

    /** The input to read from. */
    private final BsonInputStream myBsonIn;

    /** The configuration for the client. */
    private final MongoClientConfiguration myConfig;

    /** Tracks the number of sequential read timeouts. */
    private int myIdleTicks = 0;

    /** Tracks the address for the remote/far/other end of the socket. */
    private final SocketAddress myRemoteAddress;

    /** The listener for responses from the server. */
    private final TransportResponseListener myResponseListener;

    /** The transport. */
    private final AbstractSocketTransport<?> myTransport;

    /**
     * Creates a new ReceiveRunnable.
     *
     * @param config
     *            The configuration for the client.
     * @param transport
     *            The socket we are reading from.
     */
    public ReceiveRunnable(final MongoClientConfiguration config,
            final AbstractSocketTransport<?> transport) {
        myConfig = config;
        myTransport = transport;
        myBsonIn = transport.getBsonIn();
        myResponseListener = transport.getResponseListener();
        myRemoteAddress = transport.getRemoteAddress();
    }

    /**
     * Processing thread for receiving responses from the server.
     */
    @Override
    public void run() {
        try {
            while (myTransport.isOpen()) {
                try {
                    doReceiveOne();

                    // Check if we are shutdown. Note the shutdown() method
                    // makes sure the last message gets a reply.
                    if (myTransport.isShuttingDown() && myTransport.isIdle()) {
                        // All done.
                        return;
                    }
                }
                catch (final MongoDbException error) {
                    if (myTransport.isOpen()) {
                        LOG.log(Level.WARNING, "Error reading a message: "
                                + error.getMessage(), error);

                        myTransport.shutdown(
                                new ConnectionLostException(error), false);
                    }
                    // All done.
                    return;
                }
            }
        }
        finally {
            // Make sure the connection is closed completely.
            IOUtils.close(myTransport);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * If there is a pending flush then flushes.
     * </p>
     * <p>
     * If there is any available data then does a single receive.
     * </p>
     */
    @Override
    public void tryReceive() {
        try {
            if (myBsonIn.available() > 0) {
                doReceiveOne();
            }
        }
        catch (final IOException error) {
            LOG.info(
                    "Received an error when checking for pending messages: {}.",
                    error.getMessage());
        }
    }

    /**
     * Receives a single message from the connection.
     *
     * @return The {@link Message} received.
     * @throws MongoDbException
     *             On an error receiving the message.
     */
    protected Message doReceive() throws MongoDbException {
        try {
            int length;
            try {
                length = readIntSuppressTimeoutOnNonFirstByte();
            }
            catch (final SocketTimeoutException ok) {
                // This is OK. We check if we are still running and come right
                // back.
                return null;
            }

            myBsonIn.prefetch(length - 4);

            final int requestId = myBsonIn.readInt();
            final int responseId = myBsonIn.readInt();
            final int opCode = myBsonIn.readInt();

            final Operation op = Operation.fromCode(opCode);
            if (op == null) {
                // Huh? Dazed and confused
                throw new MongoDbException(new StreamCorruptedException(
                        "Unexpected operation read '" + opCode + "'."));
            }

            final Header header = new Header(length, requestId, responseId, op);
            Message message;
            switch (op) {
            case REPLY:
                message = new Reply(header, myBsonIn);
                break;
            case QUERY:
                message = new Query(header, myBsonIn);
                break;
            case UPDATE:
                message = new Update(myBsonIn);
                break;
            case INSERT:
                message = new Insert(header, myBsonIn);
                break;
            case GET_MORE:
                message = new GetMore(myBsonIn);
                break;
            case DELETE:
                message = new Delete(myBsonIn);
                break;
            case KILL_CURSORS:
                message = new KillCursors(myBsonIn);
                break;
            default:
                message = null;
                break;
            }

            return message;
        }

        catch (final IOException ioe) {
            final MongoDbException error = new ConnectionLostException(ioe);

            myTransport
            .shutdown(error, (ioe instanceof InterruptedIOException));

            throw error;
        }
    }

    /**
     * Receives and process a single message.
     */
    protected void doReceiveOne() {

        final Message received = doReceive();
        if (received != null) {
            myIdleTicks = 0;
            handle(received);
        }
        else {
            myIdleTicks += 1;

            if (myConfig.getMaxIdleTickCount() <= myIdleTicks) {
                // Shutdown the connection., nicely.
                myTransport.shutdown(new ConnectionLostException(
                        "Connection closed due to idle."), false);
            }
        }
    }

    /**
     * Process a single reply.
     *
     * @param reply
     *            The received reply.
     */
    protected void handle(final Message reply) {
        myResponseListener.response(new MessageInputBuffer(reply));
    }

    /**
     * Reads a little-endian 4 byte signed integer from the stream.
     *
     * @return The integer value.
     * @throws EOFException
     *             On insufficient data for the integer.
     * @throws IOException
     *             On a failure reading the integer.
     */
    protected int readIntSuppressTimeoutOnNonFirstByte() throws EOFException,
    IOException {
        int read = 0;
        int eofCheck = 0;
        int result = 0;

        read = myBsonIn.read();
        eofCheck |= read;
        result += (read << 0);

        for (int i = Byte.SIZE; i < Integer.SIZE; i += Byte.SIZE) {
            try {
                read = myBsonIn.read();
            }
            catch (final SocketTimeoutException ste) {
                // Bad - Only the first byte should timeout.
                throw new IOException(ste);
            }
            eofCheck |= read;
            result += (read << i);
        }

        if (eofCheck < 0) {
            throw new EOFException("Remote connection closed: "
                    + myRemoteAddress);
        }
        return result;
    }
}