/*
 * 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.sis.io.stream;

import java.io.IOException;
import java.io.EOFException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.ShortBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.nio.FloatBuffer;
import java.nio.DoubleBuffer;
import java.nio.charset.Charset;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import org.apache.sis.storage.internal.Resources;
import org.apache.sis.io.InvalidSeekException;
import org.apache.sis.util.resources.Errors;

import static org.apache.sis.util.ArgumentChecks.ensureBetween;


/**
 * Provides convenience methods for working with a ({@link ReadableByteChannel}, {@link ByteBuffer}) pair.
 * The channel and the buffer must be supplied by the caller. It is okay if they have already been used
 * before {@code ChannelDataInput} creation.
 *
 * <h2>Encapsulation</h2>
 * This class exposes publicly the {@linkplain #channel} and the {@linkplain #buffer buffer} because this class
 * is not expected to perform all possible data manipulations that we can do with the buffers. This class is only
 * a helper tool, which often needs to be completed by specialized operations performed directly on the buffer.
 * However, users are encouraged to transfer data from the channel to the buffer using only the methods provided
 * in this class if they want to keep the {@link #seek(long)} and {@link #getStreamPosition()} values accurate.
 *
 * <p>Since this class is only a helper tool, it does not "own" the channel and consequently does not provide
 * {@code close()} method. It is users responsibility to close the channel after usage.</p>
 *
 * <h2>Relationship with {@code DataInput}</h2>
 * This class API is compatibly with the {@link java.io.DataInput} interface, so subclasses can implement that
 * interface if they wish. This class does not implement {@code DataInput} itself because it is not needed for
 * SIS purposes, and because {@code DataInput} has undesirable methods ({@code readLine()} and {@code readUTF()}).
 * However, the {@link ChannelImageInputStream} class implements the {@code DataInput} interface, together with
 * the {@link javax.imageio.stream.ImageInputStream} one, mostly for situations when inter-operability with
 * {@link javax.imageio} is needed.
 *
 * @author  Martin Desruisseaux (Geomatys)
 */
public class ChannelDataInput extends ChannelData {
    /**
     * Minimum number of bytes to skip in the {@code seek(long)} operation.
     * If there is less bytes to skip, then it is not worth to do a seek
     * and we will continue reading as normal instead.
     *
     * <p>This threshold is used because some formats add padding to their data for aligning on 32 bits boundary.
     * It may result in very small seeks before reading the next chunk of data. A common padding is 32 bits,
     * but there is no harm in using a larger one here (64 bits).</p>
     */
    private static final int SEEK_THRESHOLD = Long.BYTES;

    /**
     * The channel from where data are read.
     * This is supplied at construction time.
     */
    public final ReadableByteChannel channel;

    /**
     * Creates a new data input for the given channel and using the given buffer.
     * If the buffer already contains some data, then the {@code filled} argument shall be {@code true}.
     * Otherwise (e.g. if it is a newly created buffer), then {@code filled} shall be {@code false}.
     *
     * <p><b>Tip:</b>
     * for building a data input from an input stream, see {@link InputStreamArrayGetter}.</p>
     *
     * @param  filename  a short identifier (typically a filename without path) used for formatting error message.
     * @param  channel   the channel from where data are read.
     * @param  buffer    the buffer where to copy the data.
     * @param  filled    {@code true} if the buffer already contains data, or {@code false} if it needs
     *                   to be initially filled with some content read from the channel.
     * @throws IOException if an error occurred while reading the channel.
     */
    public ChannelDataInput(final String filename, final ReadableByteChannel channel, final ByteBuffer buffer,
            final boolean filled) throws IOException
    {
        super(filename, channel, buffer);
        this.channel = channel;
        if (!filled) {
            buffer.clear();
            channel.read(buffer);
            buffer.flip();
        }
    }

    /**
     * Creates a new instance for a buffer filled with the bytes to use.
     * This constructor uses an independent, read-only view of the given buffer.
     * No reference to the given buffer will be retained.
     *
     * @param  filename  a short identifier (typically a filename without path) used for formatting error message.
     * @param  data      the buffer filled with all bytes to read.
     */
    public ChannelDataInput(final String filename, final ByteBuffer data) {
        super(filename, data);
        channel = new NullChannel();
    }

    /**
     * Creates a new input stream from the given {@code ChannelDataInput}.
     * This constructor is invoked when we need to change the implementation class.
     * The old input should not be used anymore after this constructor has been invoked.
     *
     * @param  input  the existing instance from which to takes the channel and buffer.
     */
    ChannelDataInput(final ChannelDataInput input) {
        super(input);
        channel = input.channel;
    }

    /**
     * Returns the length of the stream (in bytes), or -1 if unknown.
     * The length is relative to the channel position at {@linkplain #ChannelDataInput construction time}.
     *
     * @return the length of the stream (in bytes) relative to {@link #channelOffset}, or -1 if unknown.
     * @throws IOException if an error occurred while fetching the stream length.
     */
    public final long length() throws IOException {     // Method signature must match ImageInputStream.length().
        if (channel instanceof SeekableByteChannel) {
            return Math.subtractExact(((SeekableByteChannel) channel).size(), channelOffset);
        }
        return -1;
    }

    /**
     * Tries to read more bytes from the channel without changing the buffer position.
     * This method returns a negative number if the buffer is already full or if the channel reached the
     * <cite>end of stream</cite>. Otherwise this method reads an arbitrary number of bytes not greater
     * than the space available in the buffer, and returns the amount bytes actually read.
     *
     * @return the number of bytes read, or -2 if the buffer is full, or -1 on <cite>end of stream</cite>.
     * @throws IOException if an error occurred while reading the bytes.
     */
    public final int prefetch() throws IOException {
        final int limit    = buffer.limit();
        final int capacity = buffer.capacity();
        if (limit == capacity) {
            return -2;
        }
        final int position = buffer.position();
        buffer.limit(capacity).position(limit);
        int c = channel.read(buffer);
        while (c == 0) {
            onEmptyTransfer();
            c = channel.read(buffer);
        }
        buffer.limit(buffer.position()).position(position);
        return c;
    }

    /**
     * Returns {@code true} if the buffer or the channel has at least one byte remaining.
     * If the {@linkplain #buffer buffer} has no remaining bytes, then this method will attempts
     * to read at least one byte from the {@linkplain #channel}. If no bytes can be read because
     * the channel has reached the end of stream, then this method returns {@code false}.
     *
     * @return {@code true} if the buffer contains at least one remaining byte.
     * @throws IOException if it was necessary to read from the channel and this operation failed.
     */
    public final boolean hasRemaining() throws IOException {
        if (buffer.hasRemaining()) {
            return true;
        }
        bufferOffset += buffer.limit();
        buffer.clear();
        int c = channel.read(buffer);
        while (c == 0) {
            onEmptyTransfer();
            c = channel.read(buffer);
        }
        buffer.flip();
        return c >= 0;
    }

    /**
     * Makes sure that the buffer contains at least <var>n</var> remaining bytes.
     * It is caller's responsibility to ensure that the given number of bytes is
     * not greater than the {@linkplain ByteBuffer#capacity() buffer capacity}.
     *
     * @param  n  the minimal number of bytes needed in the {@linkplain #buffer buffer}.
     * @throws EOFException if the channel has reached the end of stream.
     * @throws IOException if another kind of error occurred while reading.
     */
    public final void ensureBufferContains(int n) throws EOFException, IOException {
        assert n >= 0 && n <= buffer.capacity() : n;
        n -= buffer.remaining();
        if (n > 0) {
            bufferOffset += buffer.position();
            buffer.compact();
            do {
                final int c = channel.read(buffer);
                if (c <= 0) {
                    if (c != 0) {
                        throw new EOFException(eof());
                    }
                    onEmptyTransfer();
                }
                n -= c;
            } while (n > 0);
            buffer.flip();
        }
    }

    /**
     * Makes sure that the buffer contains at least one remaining byte.
     *
     * @throws EOFException if the channel has reached the end of stream.
     * @throws IOException if another kind of error occurred while reading.
     */
    private void ensureNonEmpty() throws IOException {
        if (!hasRemaining()) {
            throw new EOFException(eof());
        }
    }

    /**
     * Returns the "end of file" error message, for {@link EOFException} creations.
     */
    private String eof() {
        return Errors.format(Errors.Keys.UnexpectedEndOfFile_1, filename);
    }

    /**
     * Pushes back the last processed byte. This is used when a call to {@code readBit()} did not
     * used every bits in a byte, or when {@code readLine()} checked for the Windows-style of EOL.
     */
    final void pushBack() {
        buffer.position(buffer.position() - 1);
    }

    /**
     * Reads a single bit from the stream. The bit to be read depends on the
     * {@linkplain #getBitOffset() current bit offset}.
     *
     * @return the value of the next bit from the stream.
     * @throws IOException if an error occurred while reading (including EOF).
     */
    public final int readBit() throws IOException {
        ensureBufferContains(Byte.BYTES);
        return readBitFromBuffer();
    }

    /**
     * Reads many bits from the stream. The first bit to be read depends on the
     * {@linkplain #getBitOffset() current bit offset}.
     *
     * @param  numBits  the number of bits to read.
     * @return the value of the next bits from the stream.
     * @throws IOException if an error occurred while reading (including EOF).
     */
    public final long readBits(int numBits) throws IOException {
        ensureBetween("numBits", 0, Long.SIZE, numBits);
        if (numBits == 0) {
            return 0;
        }
        /*
         * Reads the bits available in the next bytes (all of them if bitOffset == 0)
         * and compute the number of bits that still need to be read. That number may
         * be negative if we have read too many bits.
         */
        final int bitOffset = getBitOffset();
        long value = readByte() & (0xFF >>> bitOffset);
        numBits -= (Byte.SIZE - bitOffset);
        while (numBits > 0) {
            value = (value << Byte.SIZE) | readUnsignedByte();
            numBits -= Byte.SIZE;
        }
        if (numBits != 0) {
            value >>>= (-numBits);                      // Discard the unwanted bits.
            numBits += Byte.SIZE;
            pushBack();
        }
        setBitOffset(numBits);
        return value;
    }

    /**
     * Reads the next byte value (8 bits) from the stream. This method ensures that there is at
     * least 1 byte remaining in the buffer, reading new bytes from the channel if necessary,
     * then delegates to {@link ByteBuffer#get()}.
     *
     * @return the value of the next byte from the stream.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final byte readByte() throws IOException {
        ensureBufferContains(Byte.BYTES);
        return buffer.get();
    }

    /**
     * Reads the next unsigned byte value (8 bits) from the stream.
     * The implementation is as below:
     *
     * {@snippet lang="java" :
     *     return Byte.toUnsignedInt(readByte());
     *     }
     *
     * @return the value of the next unsigned byte from the stream.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final int readUnsignedByte() throws IOException {
        return Byte.toUnsignedInt(readByte());
    }

    /**
     * Reads the next short value (16 bits) from the stream. This method ensures that there is at
     * least 2 bytes remaining in the buffer, reading new bytes from the channel if necessary,
     * then delegates to {@link ByteBuffer#getShort()}.
     *
     * @return the value of the next short from the stream.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final short readShort() throws IOException {
        ensureBufferContains(Short.BYTES);
        return buffer.getShort();
    }

    /**
     * Reads the next unsigned short value (16 bits) from the stream.
     * The implementation is as below:
     *
     * {@snippet lang="java" :
     *     return Short.toUnsignedInt(readShort());
     *     }
     *
     * @return the value of the next unsigned short from the stream.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final int readUnsignedShort() throws IOException {
        return Short.toUnsignedInt(readShort());
    }

    /**
     * Reads the next character (16 bits) from the stream. This method ensures that there is at
     * least 2 bytes remaining in the buffer, reading new bytes from the channel if necessary,
     * then delegates to {@link ByteBuffer#getChar()}.
     *
     * @return the value of the next character from the stream.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final char readChar() throws IOException {
        ensureBufferContains(Character.BYTES);
        return buffer.getChar();
    }

    /**
     * Reads the next integer value (32 bits) from the stream. This method ensures that there is at
     * least 4 bytes remaining in the buffer, reading new bytes from the channel if necessary, then
     * delegates to {@link ByteBuffer#getInt()}.
     *
     * @return the value of the next integer from the stream.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final int readInt() throws IOException {
        ensureBufferContains(Integer.BYTES);
        return buffer.getInt();
    }

    /**
     * Reads the next unsigned integer value (32 bits) from the stream.
     * The implementation is as below:
     *
     * {@snippet lang="java" :
     *     return Integer.toUnsignedLong(readInt());
     *     }
     *
     * @return the value of the next unsigned integer from the stream.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final long readUnsignedInt() throws IOException {
        return Integer.toUnsignedLong(readInt());
    }

    /**
     * Reads the next long value (64 bits) from the stream. This method ensures that there is at
     * least 8 bytes remaining in the buffer, reading new bytes from the channel if necessary,
     * then delegates to {@link ByteBuffer#getLong()}.
     *
     * @return the value of the next integer from the stream.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final long readLong() throws IOException {
        ensureBufferContains(Long.BYTES);
        return buffer.getLong();
    }

    /**
     * Reads the next float value (32 bits) from the stream. This method ensures that there is at
     * least 4 bytes remaining in the buffer, reading new bytes from the channel if necessary,
     * then delegates to {@link ByteBuffer#getFloat()}.
     *
     * @return the value of the next float from the stream.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final float readFloat() throws IOException {
        ensureBufferContains(Float.BYTES);
        return buffer.getFloat();
    }

    /**
     * Reads the next double value (64 bits) from the stream. This method ensures that there is at
     * least 8 bytes remaining in the buffer, reading new bytes from the channel if necessary,
     * then delegates to {@link ByteBuffer#getDouble()}.
     *
     * @return the value of the next double from the stream.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final double readDouble() throws IOException {
        ensureBufferContains(Double.BYTES);
        return buffer.getDouble();
    }

    /**
     * Reads the given amount of bytes from the stream and returns them in a newly allocated array.
     * This is a convenience method for {@link #readFully(byte[], int, int)} with a new array.
     *
     * @param  length The number of bytes to read.
     * @return the next bytes in a newly allocated array of the given length.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final byte[] readBytes(final int length) throws IOException {
        final byte[] array = new byte[length];
        readFully(array);
        return array;
    }

    /**
     * Reads the given amount of characters from the stream and returns them in a newly allocated array.
     * This is a convenience method for {@link #readFully(char[], int, int)} with a new array.
     *
     * @param  length The number of characters to read.
     * @return the next characters in a newly allocated array of the given length.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final char[] readChars(final int length) throws IOException {
        final char[] array = new char[length];
        readFully(array, 0, length);
        return array;
    }

    /**
     * Reads the given amount of shorts from the stream and returns them in a newly allocated array.
     * This is a convenience method for {@link #readFully(short[], int, int)} with a new array.
     *
     * @param  length The number of shorts to read.
     * @return the next shorts in a newly allocated array of the given length.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final short[] readShorts(final int length) throws IOException {
        final short[] array = new short[length];
        readFully(array, 0, length);
        return array;
    }

    /**
     * Reads the given amount of integers from the stream and returns them in a newly allocated array.
     * This is a convenience method for {@link #readFully(int[], int, int)} with a new array.
     *
     * @param  length The number of integers to read.
     * @return the next integers in a newly allocated array of the given length.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final int[] readInts(final int length) throws IOException {
        final int[] array = new int[length];
        readFully(array, 0, length);
        return array;
    }

    /**
     * Reads the given amount of longs from the stream and returns them in a newly allocated array.
     * This is a convenience method for {@link #readFully(long[], int, int)} with a new array.
     *
     * @param  length The number of longs to read.
     * @return the next longs in a newly allocated array of the given length.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final long[] readLongs(final int length) throws IOException {
        final long[] array = new long[length];
        readFully(array, 0, length);
        return array;
    }

    /**
     * Reads the given amount of floats from the stream and returns them in a newly allocated array.
     * This is a convenience method for {@link #readFully(float[], int, int)} with a new array.
     *
     * @param  length The number of floats to read.
     * @return the next floats in a newly allocated array of the given length.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final float[] readFloats(final int length) throws IOException {
        final float[] array = new float[length];
        readFully(array, 0, length);
        return array;
    }

    /**
     * Reads the given amount of doubles from the stream and returns them in a newly allocated array.
     * This is a convenience method for {@link #readFully(double[], int, int)} with a new array.
     *
     * @param  length The number of doubles to read.
     * @return the next doubles in a newly allocated array of the given length.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final double[] readDoubles(final int length) throws IOException {
        final double[] array = new double[length];
        readFully(array, 0, length);
        return array;
    }

    /**
     * Reads {@code dest.length} bytes from the stream, and stores them into
     * {@code dest} starting at index 0. The implementation is as below:
     *
     * {@snippet lang="java" :
     *     return readFully(dest, 0, dest.length);
     *     }
     *
     * @param  dest An array of bytes to be written to.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final void readFully(final byte[] dest) throws IOException {
        readFully(dest, 0, dest.length);
    }

    /**
     * Reads {@code length} bytes from the stream, and stores them into
     * {@code dest} starting at index {@code offset}.
     *
     * @param  dest    an array of bytes to be written to.
     * @param  offset  the starting position within {@code dest} to write.
     * @param  length  the number of bytes to read.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final void readFully(final byte[] dest, int offset, int length) throws IOException {
        while (length != 0) {
            ensureNonEmpty();
            final int n = Math.min(buffer.remaining(), length);
            buffer.get(dest, offset, n);
            offset += n;
            length -= n;
        }
    }

    /**
     * Helper class for the {@code readFully(…)} methods,
     * in order to avoid duplicating almost identical code many times.
     */
    abstract class ArrayReader implements DataTransfer {
        /**
         * For subclass constructors only.
         */
        ArrayReader() {
        }

        /**
         * Returns a file identifier for error messages or debugging purpose.
         */
        @Override
        public final String filename() {
            return filename;
        }

        /**
         * Transfers the data from the buffer created by {@link #createView()} to array
         * of primitive Java type known by the subclass. This method may be invoked an
         * arbitrary number of time.
         */
        abstract void transfer(int offset, int n);

        /**
         * Skips the given amount of bytes in the buffer. It is caller responsibility to ensure
         * that there is enough bytes remaining in the buffer.
         */
        private void skipInBuffer(final int n) {
            buffer.position(buffer.position() + n);
        }

        /**
         * Moves to the given position in the stream.
         */
        @Override
        public final void seek(long n) throws IOException {
            ChannelDataInput.this.seek(n);
        }

        /**
         * Reads {@code length} values from the stream, and stores them into the array known to subclass,
         * starting at index {@code offset}.
         *
         * <p>If a non-null {@code Buffer} is given in argument to this method, then it must be a view over
         * the full content of {@link ChannelDataInput#buffer} (i.e. the view element at index 0 shall be
         * defined by the buffer elements starting at index 0).</p>
         *
         * @param  view     existing buffer to use as a view over {@link ChannelDataInput#buffer}, or {@code null}.
         * @param  offset   the starting position within {@code dest} to write.
         * @param  length   the number of values to read.
         * @throws IOException if an error (including EOF) occurred while reading the stream.
         */
        @Override
        public void readFully(Buffer view, int offset, int length) throws IOException {
            final int dataSizeShift = dataSizeShift();
            ensureBufferContains(Math.min(length << dataSizeShift, buffer.capacity()));
            if (view == null) {
                view = createView();                                    // Must be after ensureBufferContains(int).
            } else {
                // Buffer position must be a multiple of the data size.
                // If not, fix that by shifting the content to index 0.
                if ((buffer.position() & ((1 << dataSizeShift) - 1)) != 0) {
                    bufferOffset += buffer.position();
                    buffer.compact().flip();
                }
                view.limit   (buffer.limit()    >> dataSizeShift)
                    .position(buffer.position() >> dataSizeShift);      // See assumption documented in Javadoc.
            }
            int n = Math.min(view.remaining(), length);
            transfer(offset, n);
            skipInBuffer(n << dataSizeShift);
            while ((length -= n) != 0) {
                offset += n;
                ensureBufferContains(1 << dataSizeShift);               // Actually read as much data as possible.
                view.rewind().limit(buffer.remaining() >> dataSizeShift);
                transfer(offset, n = Math.min(view.remaining(), length));
                skipInBuffer(n << dataSizeShift);
            }
        }
    }

    /**
     * Reads bytes from the enclosing stream and stores them into the given destination array. This implementation
     * actually redirects the reading process to {@link ChannelDataInput#readFully(byte[], int, int)} because this
     * specialization does not need a view. This implementation is useless for {@code ChannelDataInput}, but avoid
     * the need to implement special cases in other classes like {@link HyperRectangleReader}.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    final class BytesReader extends ArrayReader {
        /** The array where to store the values. */      private byte[] dest;
        BytesReader(final byte[] dest)                  {this.dest = dest;}
        @Override public int    dataSizeShift()         {return 0;}
        @Override public Object dataArray()             {return dest;}
        @Override public Buffer dataArrayAsBuffer()     {return ByteBuffer.wrap(dest);}
        @Override public Buffer view()                  {return buffer;}
        @Override public Buffer createView()            {return buffer;}
        @Override public void   createDataArray(int n)  {dest = new byte[n];}
        @Override        void   transfer(int p, int n)  {buffer.get(dest, p, n);}
        @Override public void   setDest(Object array)   {dest = (byte[]) array;};
        @Override public void readFully(Buffer view, int offset, int length) throws IOException {
            ChannelDataInput.this.readFully(dest, offset, length);
        }
    };

    /**
     * Reads characters from the enclosing stream and stores them into the given destination array.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    final class CharsReader extends ArrayReader {
        /** A view over the enclosing byte buffer. */    private CharBuffer view;
        /** The array where to store the values.   */    private char[] dest;
        CharsReader(final CharBuffer source)            {this.view = source;}
        CharsReader(final char[] dest)                  {this.dest = dest;}
        @Override public int    dataSizeShift()         {return 1;}
        @Override public Object dataArray()             {return dest;}
        @Override public Buffer dataArrayAsBuffer()     {return CharBuffer.wrap(dest);}
        @Override public Buffer view()                  {return view;}
        @Override public Buffer createView()            {return view = buffer.asCharBuffer();}
        @Override public void   createDataArray(int n)  {dest = new char[n];}
        @Override        void   transfer(int p, int n)  {view.get(dest, p, n);}
        @Override public void   setDest(Object array)   {dest = (char[]) array;};
    };

    /**
     * Reads short integers from the enclosing stream and stores them into the given destination array.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    final class ShortsReader extends ArrayReader {
        /** A view over the enclosing byte buffer. */    private ShortBuffer view;
        /** The array where to store the values.   */    private short[] dest;
        ShortsReader(final ShortBuffer source)          {this.view = source;}
        ShortsReader(final short[] dest)                {this.dest = dest;}
        @Override public int    dataSizeShift()         {return 1;}
        @Override public Object dataArray()             {return dest;}
        @Override public Buffer dataArrayAsBuffer()     {return ShortBuffer.wrap(dest);}
        @Override public Buffer view()                  {return view;}
        @Override public Buffer createView()            {return view = buffer.asShortBuffer();}
        @Override public void   createDataArray(int n)  {dest = new short[n];}
        @Override void          transfer(int p, int n)  {view.get(dest, p, n);}
        @Override public void   setDest(Object array)   {dest = (short[]) array;};
    };

    /**
     * Reads integers from the enclosing stream and stores them into the given destination array.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    final class IntsReader extends ArrayReader {
        /** A view over the enclosing byte buffer. */    private IntBuffer view;
        /** The array where to store the values.   */    private int[] dest;
        IntsReader(final IntBuffer source)              {this.view = source;}
        IntsReader(final int[] dest)                    {this.dest = dest;}
        @Override public int    dataSizeShift()         {return 2;}
        @Override public Object dataArray()             {return dest;}
        @Override public Buffer dataArrayAsBuffer()     {return IntBuffer.wrap(dest);}
        @Override public Buffer view()                  {return view;}
        @Override public Buffer createView()            {return view = buffer.asIntBuffer();}
        @Override public void   createDataArray(int n)  {dest = new int[n];}
        @Override void          transfer(int p, int n)  {view.get(dest, p, n);}
        @Override public void   setDest(Object array)   {dest = (int[]) array;};
    };

    /**
     * Reads long integers from the enclosing stream and stores them into the given destination array.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    final class LongsReader extends ArrayReader {
        /** A view over the enclosing byte buffer. */    private LongBuffer view;
        /** The array where to store the values.   */    private long[] dest;
        LongsReader(final LongBuffer source)            {this.view = source;}
        LongsReader(final long[] dest)                  {this.dest = dest;}
        @Override public int    dataSizeShift()         {return 3;}
        @Override public Object dataArray()             {return dest;}
        @Override public Buffer dataArrayAsBuffer()     {return LongBuffer.wrap(dest);}
        @Override public Buffer view()                  {return view;}
        @Override public Buffer createView()            {return view = buffer.asLongBuffer();}
        @Override public void   createDataArray(int n)  {dest = new long[n];}
        @Override void          transfer(int p, int n)  {view.get(dest, p, n);}
        @Override public void   setDest(Object array)   {dest = (long[]) array;};
    };

    /**
     * Reads float values from the enclosing stream and stores them into the given destination array.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    final class FloatsReader extends ArrayReader {
        /** A view over the enclosing byte buffer. */    private FloatBuffer view;
        /** The array where to store the values.   */    private float[] dest;
        FloatsReader(final FloatBuffer source)          {this.view = source;}
        FloatsReader(final float[] dest)                {this.dest = dest;}
        @Override public int    dataSizeShift()         {return 2;}
        @Override public Object dataArray()             {return dest;}
        @Override public Buffer dataArrayAsBuffer()     {return FloatBuffer.wrap(dest);}
        @Override public Buffer view()                  {return view;}
        @Override public Buffer createView()            {return view = buffer.asFloatBuffer();}
        @Override public void   createDataArray(int n)  {dest = new float[n];}
        @Override void          transfer(int p, int n)  {view.get(dest, p, n);}
        @Override public void   setDest(Object array)   {dest = (float[]) array;};
    };

    /**
     * Reads double values from the enclosing stream and stores them into the given destination array.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    final class DoublesReader extends ArrayReader {
        /** A view over the enclosing byte buffer. */    private DoubleBuffer view;
        /** The array where to store the values.   */    private double[] dest;
        DoublesReader(final DoubleBuffer source)        {this.view = source;}
        DoublesReader(final double[] dest)              {this.dest = dest;}
        @Override public int    dataSizeShift()         {return 3;}
        @Override public Object dataArray()             {return dest;}
        @Override public Buffer dataArrayAsBuffer()     {return DoubleBuffer.wrap(dest);}
        @Override public Buffer view()                  {return view;}
        @Override public Buffer createView()            {return view = buffer.asDoubleBuffer();}
        @Override public void   createDataArray(int n)  {dest = new double[n];}
        @Override void          transfer(int p, int n)  {view.get(dest, p, n);}
        @Override public void   setDest(Object array)   {dest = (double[]) array;};
    };

    /**
     * Reads {@code length} characters from the stream, and stores them into
     * {@code dest} starting at index {@code offset}.
     *
     * @param  dest   An array of characters to be written to.
     * @param  offset The starting position within {@code dest} to write.
     * @param  length The number of characters to read.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final void readFully(final char[] dest, final int offset, final int length) throws IOException {
        new CharsReader(dest).readFully(null, offset, length);
    }

    /**
     * Reads {@code length} short integers from the stream, and stores them into
     * {@code dest} starting at index {@code offset}.
     *
     * @param  dest   An array of short integers to be written to.
     * @param  offset The starting position within {@code dest} to write.
     * @param  length The number of short integers to read.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final void readFully(final short[] dest, final int offset, final int length) throws IOException {
        new ShortsReader(dest).readFully(null, offset, length);
    }

    /**
     * Reads {@code length} integers from the stream, and stores them into
     * {@code dest} starting at index {@code offset}.
     *
     * @param  dest    an array of integers to be written to.
     * @param  offset  the starting position within {@code dest} to write.
     * @param  length  the number of integers to read.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final void readFully(final int[] dest, final int offset, final int length) throws IOException {
        new IntsReader(dest).readFully(null, offset, length);
    }

    /**
     * Reads {@code length} long integers from the stream, and stores them into
     * {@code dest} starting at index {@code offset}.
     *
     * @param  dest    an array of long integers to be written to.
     * @param  offset  the starting position within {@code dest} to write.
     * @param  length  the number of long integers to read.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final void readFully(final long[] dest, final int offset, final int length) throws IOException {
        new LongsReader(dest).readFully(null, offset, length);
    }

    /**
     * Reads {@code length} floats from the stream, and stores them into
     * {@code dest} starting at index {@code offset}.
     *
     * @param  dest    an array of floats to be written to.
     * @param  offset  the starting position within {@code dest} to write.
     * @param  length  the number of floats to read.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final void readFully(final float[] dest, final int offset, final int length) throws IOException {
        new FloatsReader(dest).readFully(null, offset, length);
    }

    /**
     * Reads {@code length} doubles from the stream, and stores them into
     * {@code dest} starting at index {@code offset}.
     *
     * @param  dest    an array of doubles to be written to.
     * @param  offset  the starting position within {@code dest} to write.
     * @param  length  the number of doubles to read.
     * @throws IOException if an error (including EOF) occurred while reading the stream.
     */
    public final void readFully(final double[] dest, final int offset, final int length) throws IOException {
        new DoublesReader(dest).readFully(null, offset, length);
    }

    /**
     * Decodes a string from a sequence of bytes in the given encoding. This method tries to avoid the creation
     * of a temporary {@code byte[]} array when possible.
     *
     * <p>This convenience method shall be used only for relatively small amount of {@link String} instances
     * to decode, for example attribute values in the file header. For large amount of data, consider using
     * {@link java.nio.charset.CharsetDecoder} instead.</p>
     *
     * @param  length    number of bytes to read.
     * @param  encoding  the character encoding.
     * @return the string decoded from the {@code length} next bytes.
     * @throws IOException if an error occurred while reading the bytes, or if the given encoding is invalid.
     */
    public final String readString(int length, final Charset encoding) throws IOException {
        final byte[] array;
        int position;
        if (buffer.hasArray() && length <= buffer.capacity()) {
            ensureBufferContains(length);
            position = buffer.position();           // Must be after `ensureBufferContains(int)`.
            buffer.position(position + length);     // Before `new String` for consistency with the `else` block in case of UnsupportedEncodingException.
            array = buffer.array();
            position += buffer.arrayOffset();
        } else {
            array = readBytes(length);
            position = 0;
        }
        while (length > 0 && array[position + (length - 1)] == 0) {
            length--;                               // Skip trailing 0 (end of string in C/C++ languages).
        }
        return new String(array, position, length, encoding);
    }

    /**
     * Moves to the given position in the stream. The given position is relative to
     * the position that the stream had at {@code ChannelDataInput} construction time.
     *
     * @param  position  the position where to move.
     * @throws IOException if the stream cannot be moved to the given position.
     */
    @Override
    public final void seek(final long position) throws IOException {
        long p = Math.subtractExact(position, bufferOffset);
        if (p >= 0 && p <= buffer.limit()) {
            /*
             * Requested position is inside the current limits of the buffer.
             */
            buffer.position((int) p);
        } else if ((p < 0 || p - buffer.limit() >= SEEK_THRESHOLD) && channel instanceof SeekableByteChannel) {
            /*
             * Requested position is outside the current limits of the buffer,
             * but we can set the new position directly in the channel.
             */
            ((SeekableByteChannel) channel).position(Math.addExact(channelOffset, position));
            bufferOffset = position;
            buffer.clear().limit(0);
        } else if (p >= 0) {
            /*
             * Requested position is after the current buffer limit and
             * we cannot seek, so we have to read everything before.
             */
            do {
                bufferOffset += buffer.limit();
                p -= buffer.limit();
                buffer.clear();
                final int c = channel.read(buffer);
                if (c <= 0) {
                    if (c != 0) {
                        throw new EOFException(eof());
                    }
                    onEmptyTransfer();
                }
                buffer.flip();
            } while (p > buffer.limit());
            buffer.position((int) p);
        } else {
            /*
             * Requested position is before the current buffer limits and we cannot seek.
             */
            throw new InvalidSeekException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, filename));
        }
        clearBitOffset();
    }

    /**
     * Specifies a range of bytes which is expected to be read.
     * The range of bytes is only a hint and may be ignored, depending on subclasses.
     * Reading more bytes than specified is okay, only potentially less efficient.
     *
     * @param  lower  position (inclusive) of the first byte to be requested.
     * @param  upper  position (exclusive) of the last byte to be requested.
     */
    public final void rangeOfInterest(long lower, long upper) {
        if (channel instanceof ByteRangeChannel) {
            lower = Math.addExact(lower, channelOffset);
            upper = Math.addExact(upper, channelOffset);
            ((ByteRangeChannel) channel).rangeOfInterest(lower, upper);
        }
    }

    /**
     * Empties the buffer and reset the channel position at the beginning of the stream.
     * This method is similar to {@code seek(0)} except that the buffer content is discarded.
     *
     * @return {@code true} on success, or {@code false} if it is not possible to reset the position.
     * @throws IOException if the stream cannot be moved to the original position.
     */
    public final boolean rewind() throws IOException {
        if (channel instanceof SeekableByteChannel) {
            ((SeekableByteChannel) channel).position(channelOffset);
            buffer.clear().limit(0);
            bufferOffset = 0;
            clearBitOffset();
            return true;
        }
        return false;
    }
}
