/*
 * 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.util.Arrays;
import java.util.Random;
import java.nio.ByteOrder;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import java.io.IOException;
import org.apache.sis.util.Numbers;
import org.apache.sis.storage.DataStoreException;

// Test dependencies
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.sis.test.TestCase;
import org.apache.sis.test.TestUtilities;


/**
 * Tests {@link HyperRectangleReader}.
 *
 * @author  Johann Sorel (Geomatys)
 * @author  Martin Desruisseaux (Geomatys)
 */
public final class HyperRectangleReaderTest extends TestCase {
    /**
     * The hyper-cube dimensions.
     */
    private final long[] size = new long[4];

    /**
     * Lower values of the sub-region to test.
     */
    private final long[] lower = new long[size.length];

    /**
     * Upper values of the sub-region to test.
     */
    private final long[] upper = new long[size.length];

    /**
     * Subsampling values to use for the test.
     */
    private final long[] subsampling = new long[size.length];

    /**
     * The reader to test for an hyper-cube of {@code short} values, created by {@link #initialize(Random, boolean)}.
     * Sample values are index values encoded in base 10. For example, the value at index (4,1,2,3) will be 4123.
     */
    private HyperRectangleReader reader;

    /**
     * Creates a new test case.
     */
    public HyperRectangleReaderTest() {
    }

    /**
     * Encodes the given index in the sample values to be stored in the array of data.
     * We use a decimal encoding for making easier to compare the actual values with the expected ones.
     */
    private static long sampleValue(final long i0, final long i1, final long i2, final long i3) {
        return i3*1000 + i2*100 + i1*10 + i0;
    }

    /**
     * Creates an hyper-rectangle of random size and initializes the sub-region and subsampling to random values.
     * Sample values are index values encoded in base 10. For example, the value at index (4,1,2,3) will be 4123.
     *
     * @param random      the random number generator to use for initializing the test.
     * @param useChannel  {@code true} for fetching the data from channel to a small buffer, or
     *                    {@code false} if the data are expected to be fully contained in the buffer.
     */
    private void initialize(final Random random, final boolean useChannel) throws IOException, DataStoreException {
        /*
         * Compute a random hyper-rectangle size, sub-region and subsampling. Each dimension will have a
         * size between 1 to 10, so we will be able to use decimal digits from 0 to 9 in the sample values.
         */
        int length = 1;
        for (int i=0; i<size.length; i++) {
            final int s = random.nextInt(9) + 1;
            int low = random.nextInt(s);
            int up  = random.nextInt(s);
            if (low > up) {
                final int t = low;
                low = up; up = t;
            }
            size [i] = s;
            lower[i] = low;
            upper[i] = up + 1;
            subsampling[i] = random.nextInt(3) + 1;
            length *= s;
        }
        /*
         * Prepare an array of bytes which will contain the short values using native byte order.
         * Put small amout of random value at the array beginning in order to test with an origin
         * different than zero.
         */
        final int origin = random.nextInt(10);
        final var array  = new byte[origin + length*Short.BYTES];
        for (int i=0; i<origin; i++) {
            array[i] = (byte) random.nextInt(0x100);
        }
        /*
         * Fill the array with short values using the encoding describes in javadoc.
         * Then wrap the array in a pseudo-channel so we can create the reader to test.
         */
        final ShortBuffer view = ByteBuffer.wrap(array, origin, length*Short.BYTES).order(ByteOrder.nativeOrder()).asShortBuffer();
        for (int i3=0; i3<size[3]; i3++) {
            for (int i2=0; i2<size[2]; i2++) {
                for (int i1=0; i1<size[1]; i1++) {
                    for (int i0=0; i0<size[0]; i0++) {
                        view.put((short) sampleValue(i0, i1, i2, i3));
                    }
                }
            }
        }
        assertEquals(length, view.position());
        if (useChannel) {
            final var channel = new ByteArrayChannel(array, true);
            final var buffer  = ByteBuffer.allocate(random.nextInt(20) + 20).order(ByteOrder.nativeOrder());
            final var input   = new ChannelDataInput("HyperRectangle in channel", channel, buffer, false);
            reader = new HyperRectangleReader(Numbers.SHORT, input);
            reader.setOrigin(origin);
        } else {
            view.clear();
            reader = new HyperRectangleReader("HyperRectangle in buffer", view);
        }
    }

    /**
     * Extracts data from a region defined by current {@link #lower}, {@link #upper} and {@link #subsampling} values,
     * then compares against the expected values.
     */
    private void verifyRegionRead() throws IOException {
        final var data = (short[]) reader.read(new Region(size, lower, upper, subsampling));
        int p = 0;
        final long s3 = subsampling[3];
        final long s2 = subsampling[2];
        final long s1 = subsampling[1];
        final long s0 = subsampling[0];
        for (long i3=lower[3]; i3<upper[3]; i3 += s3) {
            for (long i2=lower[2]; i2<upper[2]; i2 += s2) {
                for (long i1=lower[1]; i1<upper[1]; i1 += s1) {
                    for (long i0=lower[0]; i0<upper[0]; i0 += s0) {
                        assertEquals(sampleValue(i0, i1, i2, i3), data[p++], "Sample value");
                    }
                }
            }
        }
        assertEquals(p, data.length, "Array length");
    }

    /**
     * Tests reading a random part of the hyper-cube without subsampling.
     *
     * @throws IOException should never happen.
     * @throws DataStoreException should never happen.
     */
    @Test
    public void testSubRegion() throws IOException, DataStoreException {
        initialize(TestUtilities.createRandomNumberGenerator(), true);
        Arrays.fill(subsampling, 0, subsampling.length, 1);
        verifyRegionRead();
    }

    /**
     * Tests reading the full hyper-cube with a random subsampling.
     *
     * @throws IOException should never happen.
     * @throws DataStoreException should never happen.
     */
    @Test
    public void testSubSampling() throws IOException, DataStoreException {
        initialize(TestUtilities.createRandomNumberGenerator(), true);
        System.arraycopy(size, 0, upper, 0, size.length);
        Arrays.fill(lower, 0, lower.length, 0);
        verifyRegionRead();
    }

    /**
     * Tests reading a random part of the hyper-cube with a random subsampling.
     *
     * @throws IOException should never happen.
     * @throws DataStoreException should never happen.
     */
    @Test
    public void testRandom() throws IOException, DataStoreException {
        initialize(TestUtilities.createRandomNumberGenerator(), true);
        verifyRegionRead();
    }

    /**
     * Tests reading data from an existing buffer, without channel.
     *
     * @throws IOException should never happen.
     * @throws DataStoreException should never happen.
     */
    @Test
    public void testMemoryTransfer() throws IOException, DataStoreException {
        initialize(TestUtilities.createRandomNumberGenerator(), false);
        verifyRegionRead();
    }
}
