"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.swapArrayElements = exports.sumArray = exports.shuffleArrayInPlace = exports.shuffleArray = exports.setAllArrayElements = exports.isArrayInArray = exports.isArrayElementsUnique = exports.isArrayContiguous = exports.isArray = exports.includes = exports.getRandomArrayIndex = exports.getRandomArrayElementAndRemove = exports.getRandomArrayElement = exports.getLowestArrayElement = exports.getHighestArrayElement = exports.getArrayIndexes = exports.getArrayDuplicateElements = exports.getArrayCombinations = exports.filterMap = exports.emptyArray = exports.copyArray = exports.combineArrays = exports.arrayToString = exports.arrayRemoveIndexInPlace = exports.arrayRemoveIndex = exports.arrayRemoveInPlace = exports.arrayRemoveAllInPlace = exports.arrayRemoveAll = exports.arrayRemove = exports.arrayEquals = void 0;
const ReadonlySet_1 = require("../types/ReadonlySet");
const random_1 = require("./random");
const rng_1 = require("./rng");
const types_1 = require("./types");
const utils_1 = require("./utils");
/**
 * Helper function for determining if two arrays contain the exact same elements. Note that this
 * only performs a shallow comparison.
 */
function arrayEquals(array1, array2) {
    if (array1.length !== array2.length) {
        return false;
    }
    return array1.every((array1Element, i) => {
        const array2Element = array2[i];
        return array1Element === array2Element;
    });
}
exports.arrayEquals = arrayEquals;
/**
 * Builds a new array based on the original array without the specified element(s). Returns the new
 * array. If the specified element(s) are not found in the array, it will simply return a shallow
 * copy of the array.
 *
 * This function is variadic, meaning that you can specify N arguments to remove N elements.
 *
 * If there is more than one matching element in the array, this function will only remove the first
 * matching element. If you want to remove all of the elements, use the `arrayRemoveAll` function
 * instead.
 */
// eslint-disable-next-line isaacscript/no-mutable-return
function arrayRemove(originalArray, ...elementsToRemove) {
    const elementsToRemoveSet = new ReadonlySet_1.ReadonlySet(elementsToRemove);
    const array = [];
    for (const element of originalArray) {
        if (!elementsToRemoveSet.has(element)) {
            array.push(element);
        }
    }
    return array;
}
exports.arrayRemove = arrayRemove;
/**
 * Shallow copies and removes the specified element(s) from the array. Returns the copied array. If
 * the specified element(s) are not found in the array, it will simply return a shallow copy of the
 * array.
 *
 * This function is variadic, meaning that you can specify N arguments to remove N elements.
 *
 * If there is more than one matching element in the array, this function will remove every matching
 * element. If you want to only remove the first matching element, use the `arrayRemove` function
 * instead.
 */
// eslint-disable-next-line isaacscript/no-mutable-return
function arrayRemoveAll(originalArray, ...elementsToRemove) {
    const array = copyArray(originalArray);
    arrayRemoveAllInPlace(array, ...elementsToRemove);
    return array;
}
exports.arrayRemoveAll = arrayRemoveAll;
/**
 * Removes all of the specified element(s) from the array. If the specified element(s) are not found
 * in the array, this function will do nothing.
 *
 * This function is variadic, meaning that you can specify N arguments to remove N elements.
 *
 * If there is more than one matching element in the array, this function will remove every matching
 * element. If you want to only remove the first matching element, use the `arrayRemoveInPlace`
 * function instead.
 *
 * @returns True if one or more elements were removed, false otherwise.
 */
function arrayRemoveAllInPlace(
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
array, ...elementsToRemove) {
    let removedOneOrMoreElements = false;
    for (const element of elementsToRemove) {
        let index;
        do {
            index = array.indexOf(element);
            if (index > -1) {
                removedOneOrMoreElements = true;
                array.splice(index, 1);
            }
        } while (index > -1);
    }
    return removedOneOrMoreElements;
}
exports.arrayRemoveAllInPlace = arrayRemoveAllInPlace;
/**
 * Removes the specified element(s) from the array. If the specified element(s) are not found in the
 * array, this function will do nothing.
 *
 * This function is variadic, meaning that you can specify N arguments to remove N elements.
 *
 * If there is more than one matching element in the array, this function will only remove the first
 * matching element. If you want to remove all of the elements, use the `arrayRemoveAllInPlace`
 * function instead.
 *
 * @returns The removed elements. This will be an empty array if no elements were removed.
 */
// eslint-disable-next-line isaacscript/no-mutable-return
function arrayRemoveInPlace(
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
array, ...elementsToRemove) {
    const removedElements = [];
    for (const element of elementsToRemove) {
        const index = array.indexOf(element);
        if (index > -1) {
            const removedElement = array.splice(index, 1);
            removedElements.push(...removedElement);
        }
    }
    return removedElements;
}
exports.arrayRemoveInPlace = arrayRemoveInPlace;
/**
 * Shallow copies and removes the elements at the specified indexes from the array. Returns the
 * copied array. If the specified indexes are not found in the array, it will simply return a
 * shallow copy of the array.
 *
 * This function is variadic, meaning that you can specify N arguments to remove N elements.
 */
// eslint-disable-next-line isaacscript/no-mutable-return
function arrayRemoveIndex(originalArray, ...indexesToRemove) {
    const indexesToRemoveSet = new ReadonlySet_1.ReadonlySet(indexesToRemove);
    const array = [];
    for (const [i, element] of originalArray.entries()) {
        if (!indexesToRemoveSet.has(i)) {
            array.push(element);
        }
    }
    return array;
}
exports.arrayRemoveIndex = arrayRemoveIndex;
/**
 * Removes the elements at the specified indexes from the array. If the specified indexes are not
 * found in the array, this function will do nothing.
 *
 * This function is variadic, meaning that you can specify N arguments to remove N elements.
 *
 * @returns The removed elements. This will be an empty array if no elements were removed.
 */
// eslint-disable-next-line isaacscript/no-mutable-return
function arrayRemoveIndexInPlace(
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
array, ...indexesToRemove) {
    const legalIndexes = indexesToRemove.filter((i) => i >= 0 && i < array.length);
    if (legalIndexes.length === 0) {
        return [];
    }
    const legalIndexesSet = new ReadonlySet_1.ReadonlySet(legalIndexes);
    const removedElements = [];
    for (let i = array.length - 1; i >= 0; i--) {
        if (legalIndexesSet.has(i)) {
            const removedElement = array.splice(i, 1);
            removedElements.push(...removedElement);
        }
    }
    return removedElements;
}
exports.arrayRemoveIndexInPlace = arrayRemoveIndexInPlace;
function arrayToString(array) {
    if (array.length === 0) {
        return "[]";
    }
    const strings = array.map((element) => tostring(element));
    const commaSeparatedStrings = strings.join(", ");
    return `[${commaSeparatedStrings}]`;
}
exports.arrayToString = arrayToString;
/**
 * Helper function to combine two or more arrays. Returns a new array that is the composition of all
 * of the specified arrays.
 *
 * This function is variadic, meaning that you can specify N arguments to combine N arrays. Note
 * that this will only perform a shallow copy of the array elements.
 */
// eslint-disable-next-line isaacscript/no-mutable-return
function combineArrays(...arrays) {
    const elements = [];
    for (const array of arrays) {
        for (const element of array) {
            elements.push(element);
        }
    }
    return elements;
}
exports.combineArrays = combineArrays;
/**
 * Helper function to perform a shallow copy.
 *
 * @param oldArray The array to copy.
 * @param numElements Optional. If specified, will only copy the first N elements. By default, the
 *                    entire array will be copied.
 */
// eslint-disable-next-line isaacscript/no-mutable-return
function copyArray(oldArray, numElements) {
    // Using the spread operator was benchmarked to be faster than manually creating an array using
    // the below algorithm.
    if (numElements === undefined) {
        return [...oldArray];
    }
    const newArrayWithFirstNElements = [];
    for (let i = 0; i < numElements; i++) {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        newArrayWithFirstNElements.push(oldArray[i]);
    }
    return newArrayWithFirstNElements;
}
exports.copyArray = copyArray;
/** Helper function to remove all of the elements in an array in-place. */
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
function emptyArray(array) {
    array.splice(0, array.length);
}
exports.emptyArray = emptyArray;
/**
 * Helper function to perform a map and a filter at the same time. Similar to `Array.map`, provide a
 * function that transforms a value, but return `undefined` if the value should be skipped. (Thus,
 * this function cannot be used in situations where `undefined` can be a valid array element.)
 *
 * This function is useful because the `Array.map` method will always produce an array with the same
 * amount of elements as the original array.
 *
 * This is named `filterMap` after the Rust function:
 * https://doc.rust-lang.org/std/iter/struct.FilterMap.html
 */
// eslint-disable-next-line isaacscript/no-mutable-return
function filterMap(array, func) {
    const newArray = [];
    for (const element of array) {
        const newElement = func(element);
        if (newElement !== undefined) {
            newArray.push(newElement);
        }
    }
    return newArray;
}
exports.filterMap = filterMap;
/**
 * Helper function to get all possible combinations of the given array. This includes the
 * combination of an empty array.
 *
 * For example, if this function is provided an array containing 1, 2, and 3, then it will return an
 * array containing the following arrays:
 *
 * - [] (if `includeEmptyArray` is set to true)
 * - [1]
 * - [2]
 * - [3]
 * - [1, 2]
 * - [1, 3]
 * - [2, 3]
 * - [1, 2, 3]
 *
 * From: https://github.com/firstandthird/combinations/blob/master/index.js
 *
 * @param array The array to get the combinations of.
 * @param includeEmptyArray Whether to include an empty array in the combinations.
 * @param min Optional. The minimum number of elements to include in each combination. Default is 1.
 * @param max Optional. The maximum number of elements to include in each combination. Default is
 *            the length of the array.
 */
function getArrayCombinations(array, includeEmptyArray, min, max) {
    if (min === undefined || min <= 0) {
        min = 1;
    }
    if (max === undefined || max <= 0) {
        max = array.length;
    }
    const all = [];
    for (let i = min; i < array.length; i++) {
        addCombinations(i, array, [], all);
    }
    if (array.length === max) {
        all.push(array);
    }
    // Finally, account for the empty array combination.
    if (includeEmptyArray) {
        all.unshift([]);
    }
    return all;
}
exports.getArrayCombinations = getArrayCombinations;
/** Mutates the `all` array in-place. */
function addCombinations(n, src, got, 
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
all) {
    if (n === 0) {
        if (got.length > 0) {
            all[all.length] = got;
        }
        return;
    }
    for (const [i, element] of src.entries()) {
        addCombinations(n - 1, src.slice(i + 1), [...got, element], all);
    }
}
/**
 * Helper function to get the duplicate elements in an array. Only one element for each value will
 * be returned. The elements will be sorted before they are returned.
 */
function getArrayDuplicateElements(array) {
    const duplicateElements = new Set();
    const set = new Set();
    for (const element of array) {
        if (set.has(element)) {
            duplicateElements.add(element);
        }
        set.add(element);
    }
    const values = [...duplicateElements];
    return values.sort();
}
exports.getArrayDuplicateElements = getArrayDuplicateElements;
/**
 * Helper function to get an array containing the indexes of an array.
 *
 * For example, an array of `["Apple", "Banana"]` would return an array of `[0, 1]`.
 *
 * Note that normally, you would use the `Object.keys` method to get the indexes of an array, but
 * due to implementation details of TypeScriptToLua, this results in an array of 1 through N
 * (instead of an array of 0 through N -1).
 */
function getArrayIndexes(array) {
    return (0, utils_1.eRange)(array.length);
}
exports.getArrayIndexes = getArrayIndexes;
/**
 * Helper function to get the highest value in an array. Returns undefined if there were no elements
 * in the array.
 */
function getHighestArrayElement(array) {
    if (array.length === 0) {
        return undefined;
    }
    let highestValue;
    for (const element of array) {
        if (highestValue === undefined || element > highestValue) {
            highestValue = element;
        }
    }
    return highestValue;
}
exports.getHighestArrayElement = getHighestArrayElement;
/**
 * Helper function to get the lowest value in an array. Returns undefined if there were no elements
 * in the array.
 */
function getLowestArrayElement(array) {
    if (array.length === 0) {
        return undefined;
    }
    let lowestValue;
    for (const element of array) {
        if (lowestValue === undefined || element < lowestValue) {
            lowestValue = element;
        }
    }
    return lowestValue;
}
exports.getLowestArrayElement = getLowestArrayElement;
/**
 * Helper function to get a random element from the provided array.
 *
 * If you want to get an unseeded element, you must explicitly pass `undefined` to the `seedOrRNG`
 * parameter.
 *
 * @param array The array to get an element from.
 * @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
 *                  `RNG.Next` method will be called. If `undefined` is provided, it will default to
 *                  a random seed.
 * @param exceptions Optional. An array of elements to skip over if selected.
 */
function getRandomArrayElement(array, seedOrRNG, exceptions = []) {
    if (array.length === 0) {
        error("Failed to get a random array element since the provided array is empty.");
    }
    const arrayToUse = exceptions.length > 0 ? arrayRemove(array, ...exceptions) : array;
    const randomIndex = getRandomArrayIndex(arrayToUse, seedOrRNG);
    const randomElement = arrayToUse[randomIndex];
    (0, utils_1.assertDefined)(randomElement, `Failed to get a random array element since the random index of ${randomIndex} was not valid.`);
    return randomElement;
}
exports.getRandomArrayElement = getRandomArrayElement;
/**
 * Helper function to get a random element from the provided array. Once the random element is
 * decided, it is then removed from the array (in-place).
 *
 * If you want to get an unseeded element, you must explicitly pass `undefined` to the `seedOrRNG`
 * parameter.
 *
 * @param array The array to get an element from.
 * @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
 *                  `RNG.Next` method will be called. If `undefined` is provided, it will default to
 *                  a random seed.
 * @param exceptions Optional. An array of elements to skip over if selected.
 */
function getRandomArrayElementAndRemove(
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
array, seedOrRNG, exceptions = []) {
    const randomArrayElement = getRandomArrayElement(array, seedOrRNG, exceptions);
    arrayRemoveInPlace(array, randomArrayElement);
    return randomArrayElement;
}
exports.getRandomArrayElementAndRemove = getRandomArrayElementAndRemove;
/**
 * Helper function to get a random index from the provided array.
 *
 * If you want to get an unseeded index, you must explicitly pass `undefined` to the `seedOrRNG`
 * parameter.
 *
 * @param array The array to get the index from.
 * @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
 *                  `RNG.Next` method will be called. If `undefined` is provided, it will default to
 *                  a random seed.
 * @param exceptions Optional. An array of indexes that will be skipped over when getting the random
 *                   index. Default is an empty array.
 */
function getRandomArrayIndex(array, seedOrRNG, exceptions = []) {
    if (array.length === 0) {
        error("Failed to get a random array index since the provided array is empty.");
    }
    return (0, random_1.getRandomInt)(0, array.length - 1, seedOrRNG, exceptions);
}
exports.getRandomArrayIndex = getRandomArrayIndex;
/**
 * Similar to the `Array.includes` method, but works on a widened version of the array.
 *
 * This is useful when the normal `Array.includes` produces a type error from an array that uses an
 * `as const` assertion.
 */
function includes(array, searchElement) {
    const widenedArray = array;
    return widenedArray.includes(searchElement);
}
exports.includes = includes;
/**
 * Since Lua uses tables for every non-primitive data structure, it is non-trivial to determine if a
 * particular table is being used as an array. `isArray` returns true if:
 *
 * - the table contains all numerical indexes that are contiguous, starting at 1
 * - the table has no keys (i.e. an "empty" table)
 *
 * @param object The object to analyze.
 * @param ensureContiguousValues Optional. Whether the Lua table has to have all contiguous keys in
 *                               order to be considered an array. Default is true.
 */
function isArray(object, ensureContiguousValues = true) {
    if (!(0, types_1.isTable)(object)) {
        return false;
    }
    // First, if there is a metatable, this cannot be a simple array and must be a more complex
    // object.
    const metatable = getmetatable(object);
    if (metatable !== undefined) {
        return false;
    }
    // Second, handle the case of an "empty" table.
    const keys = Object.keys(object);
    if (keys.length === 0) {
        return true;
    }
    // Third, handle the case of non-numerical keys.
    const hasAllNumberKeys = keys.every((key) => (0, types_1.isNumber)(key));
    if (!hasAllNumberKeys) {
        return false;
    }
    // Fourth, check for non-contiguous elements. (Lua tables start at an index of 1.)
    if (ensureContiguousValues) {
        for (let i = 1; i <= keys.length; i++) {
            const element = object.get(i);
            if (element === undefined) {
                return false;
            }
        }
    }
    return true;
}
exports.isArray = isArray;
/**
 * Helper function to see if every element in the array is N + 1.
 *
 * For example, `[2, 3, 4]` would return true, and `[2, 3, 5]` would return false.
 */
function isArrayContiguous(array) {
    let lastValue;
    for (const element of array) {
        if (lastValue === undefined) {
            lastValue = element - 1;
        }
        if (element !== lastValue - 1) {
            return false;
        }
    }
    return true;
}
exports.isArrayContiguous = isArrayContiguous;
/**
 * Helper function to check if all the elements of an array are unique within that array.
 *
 * Under the hood, this is performed by converting the array to a set.
 */
function isArrayElementsUnique(array) {
    const set = new Set(array);
    return set.size === array.length;
}
exports.isArrayElementsUnique = isArrayElementsUnique;
/** Checks if an array is in the provided 2-dimensional array. */
function isArrayInArray(arrayToMatch, parentArray) {
    return parentArray.some((element) => arrayEquals(element, arrayToMatch));
}
exports.isArrayInArray = isArrayInArray;
/** Helper function to set every element in an array to a specific value. */
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
function setAllArrayElements(array, value) {
    for (let i = 0; i < array.length; i++) {
        array[i] = value;
    }
}
exports.setAllArrayElements = setAllArrayElements;
/**
 * Shallow copies and shuffles the array using the Fisher-Yates algorithm. Returns the copied array.
 *
 * If you want an unseeded shuffle, you must explicitly pass `undefined` to the `seedOrRNG`
 * parameter.
 *
 * From: https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
 *
 * @param originalArray The array to shuffle.
 * @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
 *                  `RNG.Next` method will be called. If `undefined` is provided, it will default to
 *                  a random seed.
 */
// eslint-disable-next-line isaacscript/no-mutable-return
function shuffleArray(originalArray, seedOrRNG) {
    const array = copyArray(originalArray);
    shuffleArrayInPlace(array, seedOrRNG);
    return array;
}
exports.shuffleArray = shuffleArray;
/**
 * Shuffles the provided array in-place using the Fisher-Yates algorithm.
 *
 * If you want an unseeded shuffle, you must explicitly pass `undefined` to the `seedOrRNG`
 * parameter.
 *
 * From: https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
 *
 * @param array The array to shuffle.
 * @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
 *                  `RNG.Next` method will be called. If `undefined` is provided, it will default to
 *                  a random seed.
 */
function shuffleArrayInPlace(
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
array, seedOrRNG) {
    let currentIndex = array.length;
    const rng = (0, rng_1.isRNG)(seedOrRNG) ? seedOrRNG : (0, rng_1.newRNG)(seedOrRNG);
    while (currentIndex > 0) {
        currentIndex--;
        const randomIndex = getRandomArrayIndex(array, rng);
        swapArrayElements(array, currentIndex, randomIndex);
    }
}
exports.shuffleArrayInPlace = shuffleArrayInPlace;
/** Helper function to sum every value in an array together. */
function sumArray(array) {
    return array.reduce((accumulator, element) => accumulator + element, 0);
}
exports.sumArray = sumArray;
/**
 * Helper function to swap two different array elements. (The elements will be swapped in-place.)
 */
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
function swapArrayElements(array, i, j) {
    const value1 = array[i]; // eslint-disable-line @typescript-eslint/no-non-null-assertion
    const value2 = array[j]; // eslint-disable-line @typescript-eslint/no-non-null-assertion
    array[i] = value2;
    array[j] = value1;
}
exports.swapArrayElements = swapArrayElements;
