/*
 * 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.io.IOException;
import java.nio.ByteOrder;
import javax.imageio.stream.IIOByteBuffer;
import javax.imageio.stream.ImageInputStream;

// Test dependencies
import static org.junit.jupiter.api.Assertions.*;


/**
 * Compares the result of two {@link ImageInputStream} instances. This class is used for comparing
 * {@link ChannelImageInputStream} with the JDK implementation, where the JDK's one is used as the
 * reference implementation.
 *
 * <p><b>This class is provided for debugging purpose only.</b> This class cannot be used in test
 * suite because it checks for identical behavior between the two input streams, which is usually
 * a too strong requirement. For example, two streams may read a different number of bytes in a call
 * to {@link #read(byte[])} and still be compliant with their contract.</p>
 *
 * @author  Rémi Maréchal (Geomatys)
 * @author  Martin Desruisseaux (Geomatys)
 */
public class ImageInputStreamComparator implements ImageInputStream {
    /**
     * The reference implementation ("expected") and the tested ("actual") input streams.
     */
    private final ImageInputStream expected, actual;

    /**
     * Creates a new comparator for the given input streams.
     *
     * @param  expected  the stream used as a reference implementation.
     * @param  actual    the stream to compare against the reference implementation.
     */
    public ImageInputStreamComparator(final ImageInputStream expected, final ImageInputStream actual) {
        this.expected = expected;
        this.actual   = actual;
    }

    /**
     * Forwards the call to the two streams.
     *
     * @param  byteOrder  the byte order to set.
     */
    @Override
    public void setByteOrder(final ByteOrder byteOrder) {
        expected.setByteOrder(byteOrder);
        actual  .setByteOrder(byteOrder);
    }

    /**
     * Forwards the call to the two streams and ensures that they return identical results.
     *
     * @return the result of the forwarded call.
     */
    @Override
    public ByteOrder getByteOrder() {
        final ByteOrder r = expected.getByteOrder();
        assertEquals(r, actual.getByteOrder());
        return r;
    }

    /**
     * Forwards the call to the two streams and ensures that they return identical results.
     *
     * @return the result of the forwarded call.
     * @throws IOException if any of the two streams failed to perform the operation.
     */
    @Override
    public int read() throws IOException {
        final int r = expected.read();
        assertEquals(r, actual.read());
        return r;
    }

    /**
     * Forwards the call to the two streams and ensures that they return identical results,
     * <strong>without tolerance for normally allowed differences</strong>.
     * This method requires that the two streams read an identical number of bytes,
     * despite the method contract allowing a different number of bytes to be read.
     *
     * @param  dest  the destination array where to store the bytes read.
     * @return the result of the forwarded call.
     * @throws IOException if any of the two streams failed to perform the operation.
     */
    @Override
    public int read(final byte[] dest) throws IOException {
        final byte[] copy = dest.clone();
        final int r = expected.read(dest);
        assertEquals(r, actual.read(copy));
        assertArrayEquals(dest, copy);
        return r;
    }

    @Override
    public int read(final byte[] dest, final int offset, final int length) throws IOException {
        final byte[] copy = dest.clone();
        final int r = expected.read(dest, offset, length);
        assertEquals(r, actual.read(copy, offset, length));
        final boolean subRange = (offset != 0) || (length != dest.length);
        assertArrayEquals(subRange ? Arrays.copyOfRange(dest, offset, offset + length) : dest,
                          subRange ? Arrays.copyOfRange(copy, offset, offset + length) : copy);
        return r;
    }

    @Override
    public void readBytes(final IIOByteBuffer dest, final int n) throws IOException {
        final IIOByteBuffer copy = new IIOByteBuffer(dest.getData().clone(), dest.getOffset(), dest.getLength());
        expected.readBytes(dest, n);
        actual  .readBytes(copy,   n);
        final int offset = dest.getOffset();
        final int length = dest.getLength();
        assertEquals(offset, copy.getOffset(), "offset");
        assertEquals(length, copy.getLength(), "length");
        assertArrayEquals(Arrays.copyOfRange(dest.getData(), offset, offset + length),
                          Arrays.copyOfRange(copy.getData(), offset, offset + length));
    }

    @Override
    public boolean readBoolean() throws IOException {
        final boolean r = expected.readBoolean();
        assertEquals(r, actual.readBoolean());
        return r;
    }

    @Override
    public byte readByte() throws IOException {
        final byte r = expected.readByte();
        assertEquals(r, actual.readByte());
        return r;
    }

    @Override
    public int readUnsignedByte() throws IOException {
        final int r = expected.readUnsignedByte();
        assertEquals(r, actual.readUnsignedByte());
        return r;
    }

    @Override
    public short readShort() throws IOException {
        final short r = expected.readShort();
        assertEquals(r, actual.readShort());
        return r;
    }

    @Override
    public int readUnsignedShort() throws IOException {
        final int r = expected.readUnsignedShort();
        assertEquals(r, actual.readUnsignedShort());
        return r;
    }

    @Override
    public char readChar() throws IOException {
        final char r = expected.readChar();
        assertEquals(r, actual.readChar());
        return r;
    }

    @Override
    public int readInt() throws IOException {
        final int r = expected.readInt();
        assertEquals(r, actual.readInt());
        return r;
    }

    @Override
    public long readUnsignedInt() throws IOException {
        final long r = expected.readUnsignedInt();
        assertEquals(r, actual.readUnsignedInt());
        return r;
    }

    @Override
    public long readLong() throws IOException {
        final long r = expected.readLong();
        assertEquals(r, actual.readLong());
        return r;
    }

    @Override
    public float readFloat() throws IOException {
        final float r = expected.readFloat();
        assertEquals(r, actual.readFloat());
        return r;
    }

    @Override
    public double readDouble() throws IOException {
        final double r = expected.readDouble();
        assertEquals(r, actual.readDouble());
        return r;
    }

    @Override
    public String readLine() throws IOException {
        final String r = expected.readLine();
        assertEquals(r, actual.readLine());
        return r;
    }

    @Override
    public String readUTF() throws IOException {
        final String r = expected.readUTF();
        assertEquals(r, actual.readUTF());
        return r;
    }

    @Override
    public void readFully(final byte[] dest) throws IOException {
        final byte[] copy = dest.clone();
        expected.readFully(dest);
        actual.readFully(copy);
        assertArrayEquals(dest, copy);
    }

    @Override
    public void readFully(final byte[] dest, final int offset, final int length) throws IOException {
        final byte[] copy = dest.clone();
        expected.readFully(dest, offset, length);
        actual  .readFully(copy, offset, length);
        final boolean subRange = (offset != 0) || (length != dest.length);
        assertArrayEquals(subRange ? Arrays.copyOfRange(dest, offset, offset + length) : dest,
                          subRange ? Arrays.copyOfRange(copy, offset, offset + length) : copy);
    }

    @Override
    public void readFully(final short[] dest, final int offset, final int length) throws IOException {
        final short[] copy = dest.clone();
        expected.readFully(dest, offset, length);
        actual  .readFully(copy, offset, length);
        final boolean subRange = (offset != 0) || (length != dest.length);
        assertArrayEquals(subRange ? Arrays.copyOfRange(dest, offset, offset + length) : dest,
                          subRange ? Arrays.copyOfRange(copy, offset, offset + length) : copy);
    }

    @Override
    public void readFully(final char[] dest, final int offset, final int length) throws IOException {
        final char[] copy = dest.clone();
        expected.readFully(dest, offset, length);
        actual  .readFully(copy, offset, length);
        final boolean subRange = (offset != 0) || (length != dest.length);
        assertArrayEquals(subRange ? Arrays.copyOfRange(dest, offset, offset + length) : dest,
                          subRange ? Arrays.copyOfRange(copy, offset, offset + length) : copy);
    }

    @Override
    public void readFully(final int[] dest, final int offset, final int length) throws IOException {
        final int[] copy = dest.clone();
        expected.readFully(dest, offset, length);
        actual  .readFully(copy, offset, length);
        final boolean subRange = (offset != 0) || (length != dest.length);
        assertArrayEquals(subRange ? Arrays.copyOfRange(dest, offset, offset + length) : dest,
                          subRange ? Arrays.copyOfRange(copy, offset, offset + length) : copy);
    }

    @Override
    public void readFully(final long[] dest, final int offset, final int length) throws IOException {
        final long[] copy = dest.clone();
        expected.readFully(dest, offset, length);
        actual  .readFully(copy, offset, length);
        final boolean subRange = (offset != 0) || (length != dest.length);
        assertArrayEquals(subRange ? Arrays.copyOfRange(dest, offset, offset + length) : dest,
                          subRange ? Arrays.copyOfRange(copy, offset, offset + length) : copy);
    }

    @Override
    public void readFully(final float[] dest, final int offset, final int length) throws IOException {
        final float[] copy = dest.clone();
        expected.readFully(dest, offset, length);
        actual  .readFully(copy, offset, length);
        final boolean subRange = (offset != 0) || (length != dest.length);
        assertArrayEquals(subRange ? Arrays.copyOfRange(dest, offset, offset + length) : dest,
                          subRange ? Arrays.copyOfRange(copy, offset, offset + length) : copy);
    }

    @Override
    public void readFully(final double[] dest, final int offset, final int length) throws IOException {
        final double[] copy = dest.clone();
        expected.readFully(dest, offset, length);
        actual  .readFully(copy, offset, length);
        final boolean subRange = (offset != 0) || (length != dest.length);
        assertArrayEquals(subRange ? Arrays.copyOfRange(dest, offset, offset + length) : dest,
                          subRange ? Arrays.copyOfRange(copy, offset, offset + length) : copy);
    }

    @Override
    public long getStreamPosition() throws IOException {
        final long r = expected.getStreamPosition();
        assertEquals(r, actual.getStreamPosition());
        return r;
    }

    @Override
    public int getBitOffset() throws IOException {
        final int r = expected.getBitOffset();
        assertEquals(r, actual.getBitOffset());
        return r;
    }

    @Override
    public void setBitOffset(int bitOffset) throws IOException {
        expected.setBitOffset(bitOffset);
        actual  .setBitOffset(bitOffset);
    }

    @Override
    public int readBit() throws IOException {
        final int r = expected.readBit();
        assertEquals(r, actual.readBit());
        return r;
    }

    @Override
    public long readBits(final int numBits) throws IOException {
        final long r = expected.readBits(numBits);
        assertEquals(r, actual.readBits(numBits));
        return r;
    }

    @Override
    public long length() throws IOException {
        final long r = expected.length();
        assertEquals(r, actual.length());
        return r;
    }

    @Override
    public int skipBytes(final int n) throws IOException {
        final int r = expected.skipBytes(n);
        assertEquals(r, actual.skipBytes(n));
        return r;
    }

    @Override
    public long skipBytes(long n) throws IOException {
        final long r = expected.skipBytes(n);
        assertEquals(r, actual.skipBytes(n));
        return r;
    }

    @Override
    public void seek(long pos) throws IOException {
        expected.seek(pos);
        actual  .seek(pos);
    }

    @Override
    public void mark() {
        expected.mark();
        actual  .mark();
    }

    @Override
    public void reset() throws IOException {
        expected.reset();
        actual  .reset();
    }

    @Override
    public void flushBefore(final long position) throws IOException {
        expected.flushBefore(position);
        actual  .flushBefore(position);
    }

    @Override
    public void flush() throws IOException {
        expected.flush();
        actual  .flush();
    }

    @Override
    public long getFlushedPosition() {
        final long r = expected.getFlushedPosition();
        assertEquals(r, actual.getFlushedPosition());
        return r;
    }

    @Override
    public boolean isCached() {
        final boolean r = expected.isCached();
        assertEquals(r, actual.isCached());
        return r;
    }

    @Override
    public boolean isCachedMemory() {
        final boolean r = expected.isCachedMemory();
        assertEquals(r, actual.isCachedMemory());
        return r;
    }

    @Override
    public boolean isCachedFile() {
        final boolean r =  expected.isCachedFile();
        assertEquals(r, actual.isCachedFile());
        return r;
    }

    @Override
    public void close() throws IOException {
        expected.close();
        actual  .close();
    }
}
