/*
 * 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
 *
 *      https://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.commons.io.channels;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.util.Objects;

import org.apache.commons.io.IOUtils;

/**
 * Works with {@link FileChannel}.
 *
 * @since 2.15.0
 */
public final class FileChannels {

    /**
     * Tests if two file channel contents are equal starting at their respective current positions.
     *
     * @param channel1       A file channel.
     * @param channel2       Another file channel.
     * @param bufferCapacity The two internal buffer capacities, in bytes.
     * @return true if the contents of both RandomAccessFiles are equal, false otherwise.
     * @throws IOException if an I/O error occurs.
     * @deprecated Use {@link #contentEquals(SeekableByteChannel, SeekableByteChannel, int)}.
     */
    @Deprecated
    public static boolean contentEquals(final FileChannel channel1, final FileChannel channel2, final int bufferCapacity) throws IOException {
        return contentEquals((SeekableByteChannel) channel1, channel2, bufferCapacity);
    }

    /**
     * Tests if two readable byte channel contents are equal starting at their respective current positions.
     *
     * @param channel1       A readable byte channel.
     * @param channel2       Another readable byte channel.
     * @param bufferCapacity The two internal buffer capacities, in bytes.
     * @return true if the contents of both RandomAccessFiles are equal, false otherwise.
     * @throws IOException if an I/O error occurs or the timeout is met.
     * @since 2.19.0
     */
    public static boolean contentEquals(final ReadableByteChannel channel1, final ReadableByteChannel channel2, final int bufferCapacity) throws IOException {
        // Before making any changes, please test with org.apache.commons.io.jmh.IOUtilsContentEqualsInputStreamsBenchmark
        // Short-circuit test
        if (Objects.equals(channel1, channel2)) {
            return true;
        }
        // Don't use ByteBuffer#compact() to avoid extra copying.
        final ByteBuffer c1Buffer = ByteBuffer.allocateDirect(bufferCapacity);
        final ByteBuffer c2Buffer = ByteBuffer.allocateDirect(bufferCapacity);
        int c1NumRead = 0;
        int c2NumRead = 0;
        boolean c1Read0 = false;
        boolean c2Read0 = false;
        // If a channel is a non-blocking channel, it may return 0 bytes read for any given call.
        while (true) {
            if (!c2Read0) {
                c1NumRead = readToLimit(channel1, c1Buffer);
                c1Buffer.clear();
                c1Read0 = c1NumRead == 0;
            }
            if (!c1Read0) {
                c2NumRead = readToLimit(channel2, c2Buffer);
                c2Buffer.clear();
                c2Read0 = c2NumRead == 0;
            }
            if (c1NumRead == IOUtils.EOF && c2NumRead == IOUtils.EOF) {
                return c1Buffer.equals(c2Buffer);
            }
            if (c1NumRead == 0 || c2NumRead == 0) {
                // 0 may be returned from a non-blocking channel.
                Thread.yield();
                continue;
            }
            if (c1NumRead != c2NumRead || !c1Buffer.equals(c2Buffer)) {
                return false;
            }
        }
    }

    /**
     * Tests if two seekable byte channel contents are equal starting at their respective current positions.
     * <p>
     * If the two channels have different sizes, no content comparison takes place, and this method returns false.
     * </p>
     *
     * @param channel1       A seekable byte channel.
     * @param channel2       Another seekable byte channel.
     * @param bufferCapacity The two internal buffer capacities, in bytes.
     * @return true if the contents of both RandomAccessFiles are equal, false otherwise.
     * @throws IOException if an I/O error occurs or the timeout is met.
     * @since 2.19.0
     */
    public static boolean contentEquals(final SeekableByteChannel channel1, final SeekableByteChannel channel2, final int bufferCapacity) throws IOException {
        // Short-circuit test
        if (Objects.equals(channel1, channel2)) {
            return true;
        }
        // Short-circuit test
        final long size1 = size(channel1);
        final long size2 = size(channel2);
        if (size1 != size2) {
            return false;
        }
        return size1 == 0 && size2 == 0 || contentEquals((ReadableByteChannel) channel1, channel2, bufferCapacity);
    }

    /**
     * Reads a sequence of bytes from a channel into the given buffer until the buffer reaches its limit or the channel has reaches end-of-stream.
     * <p>
     * The buffer's limit is not changed.
     * </p>
     *
     * @param channel The source channel.
     * @param dst     The buffer into which bytes are to be transferred.
     * @return The number of bytes read, <em>never</em> zero, or {@code -1} if the channel has reached end-of-stream.
     * @throws IOException              If some other I/O error occurs.
     * @throws IllegalArgumentException If there is room in the given buffer.
     */
    private static int readToLimit(final ReadableByteChannel channel, final ByteBuffer dst) throws IOException {
        if (!dst.hasRemaining()) {
            throw new IllegalArgumentException();
        }
        int totalRead = 0;
        while (dst.hasRemaining()) {
            final int numRead;
            if ((numRead = channel.read(dst)) == IOUtils.EOF) {
                break;
            }
            if (numRead == 0) {
                // 0 may be returned from a non-blocking channel.
                Thread.yield();
            } else {
                totalRead += numRead;
            }
        }
        return totalRead != 0 ? totalRead : IOUtils.EOF;
    }

    private static long size(final SeekableByteChannel channel) throws IOException {
        return channel != null ? channel.size() : 0;
    }

    /**
     * Don't instantiate.
     */
    private FileChannels() {
        // no-op
    }
}
