/*
 * Copyright (c) 2008-2014 MongoDB, 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.
 */

package org.bson.io;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;

/**
 * Utility class for reading values from an input stream.
 */
public class Bits {

    /**
     * Reads bytes from the input stream and puts them into the given byte buffer. The equivalent of calling
     * {@link #readFully(java.io.InputStream, byte[], int, int)} with an offset of zero and a length equal to the length of the buffer.
     *
     * @param inputStream the input stream to read from
     * @param buffer      the buffer into which the data is read.
     * @throws IOException if there's an error reading from the {@code inputStream}
     */
    public static void readFully(final InputStream inputStream, final byte[] buffer)
    throws IOException {
        readFully(inputStream, buffer, buffer.length);
    }

    /**
     * Reads bytes from the input stream and puts them into the given byte buffer. The equivalent of calling
     * {@link #readFully(java.io.InputStream, byte[], int, int)} with an offset of zero.
     *
     * @param inputStream the input stream to read from
     * @param buffer      the buffer into which the data is read.
     * @param length      the maximum number of bytes to read.
     * @throws IOException if there's an error reading from the {@code inputStream}
     */
    public static void readFully(final InputStream inputStream, final byte[] buffer, final int length)
    throws IOException {
        readFully(inputStream, buffer, 0, length);
    }

    /**
     * Reads bytes from the input stream and puts them into the given byte buffer.
     *
     * @param inputStream the input stream to read from
     * @param buffer      the buffer into which the data is read.
     * @param offset      the start offset in array {@code buffer} at which the data is written.
     * @param length      the maximum number of bytes to read.
     * @throws IOException if there's an error reading from the {@code inputStream}
     * @see java.io.InputStream#read(byte[], int, int)
     */
    public static void readFully(final InputStream inputStream, final byte[] buffer, final int offset, final int length)
    throws IOException {
        if (buffer.length < length + offset) {
            throw new IllegalArgumentException("Buffer is too small");
        }

        int arrayOffset = offset;
        int bytesToRead = length;
        while (bytesToRead > 0) {
            int bytesRead = inputStream.read(buffer, arrayOffset, bytesToRead);
            if (bytesRead < 0) {
                throw new EOFException();
            }
            bytesToRead -= bytesRead;
            arrayOffset += bytesRead;
        }
    }

    /**
     * Reads and returns a single integer value from the input stream.
     *
     * @param inputStream the input stream to read from
     * @return the integer value
     * @throws IOException if there's an error reading from the {@code inputStream}
     */
    public static int readInt(final InputStream inputStream) throws IOException {
        return readInt(inputStream, new byte[4]);
    }

    /**
     * Reads and returns a single integer value from the input stream.
     *
     * @param inputStream the input stream to read from
     * @param buffer the buffer to write the input stream bytes into
     * @return the integer value
     * @throws IOException if there's an error reading from the {@code inputStream}
     */
    public static int readInt(final InputStream inputStream, final byte[] buffer) throws IOException {
        readFully(inputStream, buffer, 4);
        return readInt(buffer);
    }

    /**
     * Reads and returns a single integer value from the buffer. The equivalent of calling {@link #readInt(byte[], int)}
     * with an offset of zero.
     *
     * @param buffer the buffer to read from
     * @return the integer value
     */
    public static int readInt(final byte[] buffer) {
        return readInt(buffer, 0);
    }

    /**
     * Reads and returns a single integer value from the buffer.
     *
     * @param buffer the buffer to read from
     * @param offset the position to start reading from the buffer
     * @return the integer value
     */
    public static int readInt(final byte[] buffer, final int offset) {
        int x = 0;
        x |= (0xFF & buffer[offset + 0]) << 0;
        x |= (0xFF & buffer[offset + 1]) << 8;
        x |= (0xFF & buffer[offset + 2]) << 16;
        x |= (0xFF & buffer[offset + 3]) << 24;
        return x;
    }

    /**
     * Reads and returns a single big-endian integer value
     *
     * @param buffer the buffer to read from
     * @param offset the position to start reading from the buffer
     * @return the integer value
     */
    public static int readIntBE(final byte[] buffer, final int offset) {
        int x = 0;
        x |= (0xFF & buffer[offset + 0]) << 24;
        x |= (0xFF & buffer[offset + 1]) << 16;
        x |= (0xFF & buffer[offset + 2]) << 8;
        x |= (0xFF & buffer[offset + 3]) << 0;
        return x;
    }

    /**
     * Reads and returns a single long value from the input stream.
     *
     * @param inputStream the input stream to read from
     * @return the long value
     * @throws IOException if there's an error reading from the {@code inputStream}
     */
    public static long readLong(final InputStream inputStream) throws IOException {
        return readLong(inputStream, new byte[8]);
    }

    /**
     * Reads and returns a single long value from the input stream.
     *
     * @param inputStream the input stream to read from
     * @param buffer the buffer to write the input stream bytes into
     * @return the long value
     * @throws IOException if there's an error reading from the {@code inputStream}
     */
    public static long readLong(final InputStream inputStream, final byte[] buffer) throws IOException {
        readFully(inputStream, buffer, 8);
        return readLong(buffer);
    }

    /**
     * Reads and returns a single long value from the buffer. The equivalent of called {@link #readLong(byte[], int)} with an offset of
     * zero.
     *
     * @param buffer the buffer to read from
     * @return the long value
     */
    public static long readLong(final byte[] buffer) {
        return readLong(buffer, 0);
    }

    /**
     * Reads and returns a single long value from the buffer.
     *
     * @param buffer the buffer to read from
     * @param offset the position to start reading from the buffer
     * @return the long value
     */
    public static long readLong(final byte[] buffer, final int offset) {
        long x = 0;
        x |= (0xFFL & buffer[offset + 0]) << 0;
        x |= (0xFFL & buffer[offset + 1]) << 8;
        x |= (0xFFL & buffer[offset + 2]) << 16;
        x |= (0xFFL & buffer[offset + 3]) << 24;
        x |= (0xFFL & buffer[offset + 4]) << 32;
        x |= (0xFFL & buffer[offset + 5]) << 40;
        x |= (0xFFL & buffer[offset + 6]) << 48;
        x |= (0xFFL & buffer[offset + 7]) << 56;
        return x;
    }
}
