/*
 * 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.commons.rng.sampling;

import org.apache.commons.rng.UniformRandomProvider;

/**
 * Utilities for shuffling an array in-place.
 *
 * <p>Shuffles use the <a
 * href="https://en.wikipedia.org/wiki/Fisher-Yates_shuffle#The_modern_algorithm">
 * Fisher-Yates</a> algorithm.
 *
 * @since 1.6
 */
public final class ArraySampler {
    /** Class contains only static methods. */
    private ArraySampler() {}

    /**
     * Shuffles the entries of the given array.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @return a reference to the given array
     */
    public static boolean[] shuffle(UniformRandomProvider rng, boolean[] array) {
        for (int i = array.length; i > 1; i--) {
            swap(array, i - 1, rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @return a reference to the given array
     */
    public static byte[] shuffle(UniformRandomProvider rng, byte[] array) {
        for (int i = array.length; i > 1; i--) {
            swap(array, i - 1, rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @return a reference to the given array
     */
    public static char[] shuffle(UniformRandomProvider rng, char[] array) {
        for (int i = array.length; i > 1; i--) {
            swap(array, i - 1, rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @return a reference to the given array
     */
    public static double[] shuffle(UniformRandomProvider rng, double[] array) {
        for (int i = array.length; i > 1; i--) {
            swap(array, i - 1, rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @return a reference to the given array
     */
    public static float[] shuffle(UniformRandomProvider rng, float[] array) {
        for (int i = array.length; i > 1; i--) {
            swap(array, i - 1, rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @return a reference to the given array
     */
    public static int[] shuffle(UniformRandomProvider rng, int[] array) {
        for (int i = array.length; i > 1; i--) {
            swap(array, i - 1, rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @return a reference to the given array
     */
    public static long[] shuffle(UniformRandomProvider rng, long[] array) {
        for (int i = array.length; i > 1; i--) {
            swap(array, i - 1, rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @return a reference to the given array
     */
    public static short[] shuffle(UniformRandomProvider rng, short[] array) {
        for (int i = array.length; i > 1; i--) {
            swap(array, i - 1, rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array.
     *
     * @param <T> Type of the items.
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @return a reference to the given array
     */
    public static <T> T[] shuffle(UniformRandomProvider rng, T[] array) {
        for (int i = array.length; i > 1; i--) {
            swap(array, i - 1, rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array in the range {@code [from, to)}.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @param from Lower-bound (inclusive) of the sub-range.
     * @param to Upper-bound (exclusive) of the sub-range.
     * @return a reference to the given array
     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
     */
    public static boolean[] shuffle(UniformRandomProvider rng, boolean[] array, int from, int to) {
        final int length = to - checkFromToIndex(from, to, array.length);
        for (int i = length; i > 1; i--) {
            swap(array, from + i - 1, from + rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array in the range {@code [from, to)}.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @param from Lower-bound (inclusive) of the sub-range.
     * @param to Upper-bound (exclusive) of the sub-range.
     * @return a reference to the given array
     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
     */
    public static byte[] shuffle(UniformRandomProvider rng, byte[] array, int from, int to) {
        final int length = to - checkFromToIndex(from, to, array.length);
        for (int i = length; i > 1; i--) {
            swap(array, from + i - 1, from + rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array in the range {@code [from, to)}.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @param from Lower-bound (inclusive) of the sub-range.
     * @param to Upper-bound (exclusive) of the sub-range.
     * @return a reference to the given array
     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
     */
    public static char[] shuffle(UniformRandomProvider rng, char[] array, int from, int to) {
        final int length = to - checkFromToIndex(from, to, array.length);
        for (int i = length; i > 1; i--) {
            swap(array, from + i - 1, from + rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array in the range {@code [from, to)}.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @param from Lower-bound (inclusive) of the sub-range.
     * @param to Upper-bound (exclusive) of the sub-range.
     * @return a reference to the given array
     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
     */
    public static double[] shuffle(UniformRandomProvider rng, double[] array, int from, int to) {
        final int length = to - checkFromToIndex(from, to, array.length);
        for (int i = length; i > 1; i--) {
            swap(array, from + i - 1, from + rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array in the range {@code [from, to)}.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @param from Lower-bound (inclusive) of the sub-range.
     * @param to Upper-bound (exclusive) of the sub-range.
     * @return a reference to the given array
     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
     */
    public static float[] shuffle(UniformRandomProvider rng, float[] array, int from, int to) {
        final int length = to - checkFromToIndex(from, to, array.length);
        for (int i = length; i > 1; i--) {
            swap(array, from + i - 1, from + rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array in the range {@code [from, to)}.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @param from Lower-bound (inclusive) of the sub-range.
     * @param to Upper-bound (exclusive) of the sub-range.
     * @return a reference to the given array
     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
     */
    public static int[] shuffle(UniformRandomProvider rng, int[] array, int from, int to) {
        final int length = to - checkFromToIndex(from, to, array.length);
        for (int i = length; i > 1; i--) {
            swap(array, from + i - 1, from + rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array in the range {@code [from, to)}.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @param from Lower-bound (inclusive) of the sub-range.
     * @param to Upper-bound (exclusive) of the sub-range.
     * @return a reference to the given array
     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
     */
    public static long[] shuffle(UniformRandomProvider rng, long[] array, int from, int to) {
        final int length = to - checkFromToIndex(from, to, array.length);
        for (int i = length; i > 1; i--) {
            swap(array, from + i - 1, from + rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array in the range {@code [from, to)}.
     *
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @param from Lower-bound (inclusive) of the sub-range.
     * @param to Upper-bound (exclusive) of the sub-range.
     * @return a reference to the given array
     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
     */
    public static short[] shuffle(UniformRandomProvider rng, short[] array, int from, int to) {
        final int length = to - checkFromToIndex(from, to, array.length);
        for (int i = length; i > 1; i--) {
            swap(array, from + i - 1, from + rng.nextInt(i));
        }
        return array;
    }

    /**
     * Shuffles the entries of the given array in the range {@code [from, to)}.
     *
     * @param <T> Type of the items.
     * @param rng Source of randomness.
     * @param array Array whose entries will be shuffled (in-place).
     * @param from Lower-bound (inclusive) of the sub-range.
     * @param to Upper-bound (exclusive) of the sub-range.
     * @return a reference to the given array
     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
     */
    public static <T> T[] shuffle(UniformRandomProvider rng, T[] array, int from, int to) {
        final int length = to - checkFromToIndex(from, to, array.length);
        for (int i = length; i > 1; i--) {
            swap(array, from + i - 1, from + rng.nextInt(i));
        }
        return array;
    }

    /**
     * Swaps the two specified elements in the array.
     *
     * @param array Array.
     * @param i First index.
     * @param j Second index.
     */
    private static void swap(boolean[] array, int i, int j) {
        final boolean tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    /**
     * Swaps the two specified elements in the array.
     *
     * @param array Array.
     * @param i First index.
     * @param j Second index.
     */
    private static void swap(byte[] array, int i, int j) {
        final byte tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    /**
     * Swaps the two specified elements in the array.
     *
     * @param array Array.
     * @param i First index.
     * @param j Second index.
     */
    private static void swap(char[] array, int i, int j) {
        final char tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    /**
     * Swaps the two specified elements in the array.
     *
     * @param array Array.
     * @param i First index.
     * @param j Second index.
     */
    private static void swap(double[] array, int i, int j) {
        final double tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    /**
     * Swaps the two specified elements in the array.
     *
     * @param array Array.
     * @param i First index.
     * @param j Second index.
     */
    private static void swap(float[] array, int i, int j) {
        final float tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    /**
     * Swaps the two specified elements in the array.
     *
     * @param array Array.
     * @param i First index.
     * @param j Second index.
     */
    private static void swap(int[] array, int i, int j) {
        final int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    /**
     * Swaps the two specified elements in the array.
     *
     * @param array Array.
     * @param i First index.
     * @param j Second index.
     */
    private static void swap(long[] array, int i, int j) {
        final long tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    /**
     * Swaps the two specified elements in the array.
     *
     * @param array Array.
     * @param i First index.
     * @param j Second index.
     */
    private static void swap(short[] array, int i, int j) {
        final short tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    /**
     * Swaps the two specified elements in the array.
     *
     * @param array Array.
     * @param i First index.
     * @param j Second index.
     */
    private static void swap(Object[] array, int i, int j) {
        final Object tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    /**
     * Checks if the sub-range from fromIndex (inclusive) to toIndex (exclusive) is
     * within the bounds of range from 0 (inclusive) to length (exclusive).
     *
     * <p>This function provides the functionality of
     * {@code java.utils.Objects.checkFromToIndex} introduced in JDK 9. The <a
     * href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Objects.html#checkFromToIndex(int,int,int)">Objects</a>
     * javadoc has been reproduced for reference.
     *
     * <p>The sub-range is defined to be out of bounds if any of the following
     * inequalities is true:
     * <ul>
     * <li>{@code fromIndex < 0}
     * <li>{@code fromIndex > toIndex}
     * <li>{@code toIndex > length}
     * <li>{@code length < 0}, which is implied from the former inequalities
     * </ul>
     *
     * @param fromIndex Lower-bound (inclusive) of the sub-range.
     * @param toIndex Upper-bound (exclusive) of the sub-range.
     * @param length Upper-bound (exclusive) of the range
     * @return fromIndex if the sub-range is within the bounds of the range
     * @throws IndexOutOfBoundsException if the sub-range is out of bounds
     */
    private static int checkFromToIndex(int fromIndex, int toIndex, int length) {
        // Checks as documented above
        if (fromIndex < 0 || fromIndex > toIndex || toIndex > length) {
            throw new IndexOutOfBoundsException(
                    String.format("Range [%d, %d) out of bounds for length %d", fromIndex, toIndex, length));
        }
        return fromIndex;
    }
}
