/*
 * 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.internal.storage.io;

import java.io.DataInput;
import java.io.DataInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import org.junit.Test;

import static org.junit.Assert.*;


/**
 * Tests {@link ChannelDataInput}. First, a buffer is filled with random data. Then, a view over a portion
 * of that buffer is used for the tests, while the original full buffer is used for comparison purpose.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 0.5
 * @since   0.3
 * @module
 */
public final strictfp class ChannelDataInputTest extends ChannelDataTestCase {
    /**
     * The implementation to test.
     */
    private ChannelDataInput testedStream;

    /**
     * A stream to use as a reference implementation.
     */
    private DataInput referenceStream;

    /**
     * Fills a buffer with random data and compare the result with a standard image input stream.
     * We allocate a small buffer for the {@code ChannelDataInput} in order to force frequent
     * interactions between the buffer and the channel.
     *
     * @throws IOException should never happen since we read and write in memory only.
     */
    @Test
    public void testAllReadMethods() throws IOException {
        final byte[] array = createRandomArray(STREAM_LENGTH);
        referenceStream = new DataInputStream(new ByteArrayInputStream(array));
        testedStream = new ChannelDataInput("testAllReadMethods", new DripByteChannel(array, random, 1, 1024),
                ByteBuffer.allocate(random.nextInt(BUFFER_MAX_CAPACITY) + Double.BYTES), false);
        transferRandomData(testedStream, array.length - ARRAY_MAX_LENGTH, 16);
    }

    /**
     * Reads a random unit of data using a method selected randomly.
     * This method is invoked (indirectly) by {@link #testAllReadMethods()}.
     */
    @Override
    final void transferRandomData(final int operation) throws IOException {
        final ChannelDataInput t = testedStream;
        final DataInput r = referenceStream;
        switch (operation) {
            default: throw new AssertionError(operation);
            case  0: assertEquals("readByte()",          r.readByte(),              t.readByte());          break;
            case  1: assertEquals("readShort()",         r.readShort(),             t.readShort());         break;
            case  2: assertEquals("readUnsignedShort()", r.readUnsignedShort(),     t.readUnsignedShort()); break;
            case  3: assertEquals("readChar()",          r.readChar(),              t.readChar());          break;
            case  4: assertEquals("readInt()",           r.readInt(),               t.readInt());           break;
            case  5: assertEquals("readUnsignedInt()",   r.readInt() & 0xFFFFFFFFL, t.readUnsignedInt());   break;
            case  6: assertEquals("readLong()",          r.readLong(),              t.readLong());          break;
            case  7: assertEquals("readFloat()",         r.readFloat(),             t.readFloat(),  0f);    break;
            case  8: assertEquals("readDouble()",        r.readDouble(),            t.readDouble(), 0d);    break;
            case  9: {
                final int n = random.nextInt(ARRAY_MAX_LENGTH);
                final byte[] tmp = new byte[n];
                r.readFully(tmp);
                assertArrayEquals("readBytes(int)", tmp, t.readBytes(n));
                break;
            }
            case 10: {
                final int n = random.nextInt(ARRAY_MAX_LENGTH / Character.BYTES);
                final char[] tmp = new char[n];
                for (int i=0; i<n; i++) tmp[i] = r.readChar();
                assertArrayEquals("readChars(int)", tmp, t.readChars(n));
                break;
            }
            case 11: {
                final int n = random.nextInt(ARRAY_MAX_LENGTH / Short.BYTES);
                final short[] tmp = new short[n];
                for (int i=0; i<n; i++) tmp[i] = r.readShort();
                assertArrayEquals("readShorts(int)", tmp, t.readShorts(n));
                break;
            }
            case 12: {
                final int n = random.nextInt(ARRAY_MAX_LENGTH / Integer.BYTES);
                final int[] tmp = new int[n];
                for (int i=0; i<n; i++) tmp[i] = r.readInt();
                assertArrayEquals("readInts(int)", tmp, t.readInts(n));
                break;
            }
            case 13: {
                final int n = random.nextInt(ARRAY_MAX_LENGTH / Long.BYTES);
                final long[] tmp = new long[n];
                for (int i=0; i<n; i++) tmp[i] = r.readLong();
                assertArrayEquals("readLongs(int)", tmp, t.readLongs(n));
                break;
            }
            case 14: {
                final int n = random.nextInt(ARRAY_MAX_LENGTH / Float.BYTES);
                final float[] tmp = new float[n];
                for (int i=0; i<n; i++) tmp[i] = r.readFloat();
                assertArrayEquals("readFloats(int)", tmp, t.readFloats(n), 0);
                break;
            }
            case 15: {
                final int n = random.nextInt(ARRAY_MAX_LENGTH / Double.BYTES);
                final double[] tmp = new double[n];
                for (int i=0; i<n; i++) tmp[i] = r.readDouble();
                assertArrayEquals("readDoubles(int)", tmp, t.readDoubles(n), 0);
                break;
            }
        }
    }

    /**
     * Tests the {@link ChannelDataInput#readString(int, Charset)} method.
     *
     * @throws IOException should never happen since we read and write in memory only.
     */
    @Test
    public void testReadString() throws IOException {
        final String expected = "お元気ですか";
        final byte[] array    = expected.getBytes("UTF-8");
        assertEquals(expected.length()*3, array.length); // Sanity check.
        final ChannelDataInput input = new ChannelDataInput("testReadString",
                new DripByteChannel(array, random, 1, 32),
                ByteBuffer.allocate(array.length + 4), false);
        assertEquals(expected, input.readString(array.length, StandardCharsets.UTF_8));
        assertFalse(input.buffer.hasRemaining());
    }

    /**
     * Tests {@link ChannelDataInput#seek(long)} on a channel that do not implement
     * {@link java.nio.channels.SeekableByteChannel}.
     *
     * @throws IOException should never happen since we read and write in memory only.
     */
    @Test
    public void testSeekOnForwardOnlyChannel() throws IOException {
        int length = random.nextInt(2048) + 1024;
        final byte[] array = createRandomArray(length);
        length -= Long.BYTES; // Safety against buffer underflow.
        final ByteBuffer buffer = ByteBuffer.wrap(array);
        final ChannelDataInput input = new ChannelDataInput("testSeekOnForwardOnlyChannel",
                new DripByteChannel(array, random, 1, 2048),
                ByteBuffer.allocate(random.nextInt(64) + 16), false);
        int position = 0;
        while (position < length) {
            input.seek(position);
            assertEquals("getStreamPosition()", position, input.getStreamPosition());
            assertEquals(buffer.getLong(position), input.readLong());
            position += random.nextInt(128);
        }
    }

    /**
     * Tests {@link ChannelDataInput#prefetch()}.
     *
     * @throws IOException should never happen since we read and write in memory only.
     */
    @Test
    public void testPrefetch() throws IOException {
        final int        length = random.nextInt(256) + 128;
        final byte[]     array  = createRandomArray(length);
        final ByteBuffer buffer = ByteBuffer.allocate(random.nextInt(64) + 16);
        final ChannelDataInput input = new ChannelDataInput("testPrefetch",
                new DripByteChannel(array, random, 1, 64), buffer, false);
        int position = 0;
        while (position != length) {
            if (random.nextBoolean()) {
                assertEquals(array[position++], input.readByte());
            }
            /*
             * Prefetch a random amount of bytes and verifies the buffer status.
             */
            final int p = buffer.position();
            final int m = buffer.limit();
            final int n = input.prefetch();
            assertEquals("Position shall be unchanged.", p, buffer.position());
            final int limit = buffer.limit();
            if (n >= 0) {
                // Usual case.
                assertTrue("Limit shall be increased.", limit > m);
            } else {
                // Buffer is full or channel reached the end of stream.
                assertEquals("Limit shall be unchanged", m, limit);
            }
            /*
             * Compare the buffer content with the original data array. The comparison starts
             * from the buffer begining, in order to ensure that previous data are unchanged.
             */
            final int offset = position - buffer.position();
            for (int i=0; i<limit; i++) {
                assertEquals(array[offset + i], buffer.get(i));
            }
        }
    }
}
