/**
 * @file Helper module for `Tensor` processing.
 *
 * These functions and classes are only used internally,
 * meaning an end-user shouldn't need to access anything here.
 *
 * @module utils/tensor
 */

import { interpolate_data, max, min, permute_data, uint16_to_float32 } from './maths.js';

import { Tensor as ONNXTensor, isONNXTensor } from '../backends/onnx.js';

import { TensorOpRegistry } from '../ops/registry.js';

import { DataTypeMap } from './dtypes.js';

import { random } from './random.js';

/**
 * @typedef {keyof typeof DataTypeMap} DataType
 * @typedef {import('./maths.js').AnyTypedArray | any[]} DataArray
 */

export class Tensor {
    /**
     * Dimensions of the tensor.
     * @type {number[]}
     */
    get dims() {
        // @ts-ignore
        return this.ort_tensor.dims;
    }
    set dims(value) {
        // FIXME: ONNXTensor declares dims as readonly so one needs to use the constructor() if dims change.
        // @ts-ignore
        this.ort_tensor.dims = value;
    }

    /**
     * Type of the tensor.
     * @type {DataType}
     */
    get type() {
        return this.ort_tensor.type;
    }

    /**
     * The data stored in the tensor.
     * @type {DataArray}
     */
    get data() {
        return this.ort_tensor.data;
    }

    /**
     * The number of elements in the tensor.
     * @type {number}
     */
    get size() {
        return this.ort_tensor.size;
    }

    /**
     * The location of the tensor data.
     * @type {string}
     */
    get location() {
        return this.ort_tensor.location;
    }

    ort_tensor;

    /**
     * Create a new Tensor or copy an existing Tensor.
     * @param {[DataType, DataArray, number[]]|[ONNXTensor]} args
     */
    constructor(...args) {
        if (isONNXTensor(args[0])) {
            this.ort_tensor = /** @type {ONNXTensor} */ (args[0]);
        } else {
            // Create new tensor
            this.ort_tensor = new ONNXTensor(
                /** @type {DataType} */ (args[0]),
                // @ts-expect-error ts(2769) Type 'number' is not assignable to type 'bigint'.
                /** @type {Exclude<import('./maths.js').AnyTypedArray, Uint8ClampedArray>} */ (args[1]),
                args[2],
            );
        }

        return new Proxy(this, {
            get: (obj, key) => {
                if (typeof key === 'string') {
                    let index = Number(key);
                    if (Number.isInteger(index)) {
                        // key is an integer (i.e., index)
                        return obj._getitem(index);
                    }
                }
                // @ts-ignore
                return obj[key];
            },
            set: (obj, key, value) => {
                // TODO allow setting of data

                // @ts-ignore
                return (obj[key] = value);
            },
        });
    }

    dispose() {
        this.ort_tensor.dispose();
        // this.ort_tensor = undefined;
    }

    /**
     * Returns an iterator object for iterating over the tensor data in row-major order.
     * If the tensor has more than one dimension, the iterator will yield subarrays.
     * @returns {Iterator<any>} An iterator object for iterating over the tensor data in row-major order.
     */
    *[Symbol.iterator]() {
        const [iterLength, ...iterDims] = this.dims;

        if (iterDims.length > 0) {
            const iterSize = iterDims.reduce((a, b) => a * b);
            for (let i = 0; i < iterLength; ++i) {
                yield this._subarray(i, iterSize, iterDims);
            }
        } else {
            yield* this.data;
        }
    }

    /**
     * Index into a Tensor object.
     * @param {number} index The index to access.
     * @returns {Tensor} The data at the specified index.
     */
    _getitem(index) {
        const [iterLength, ...iterDims] = this.dims;

        index = safeIndex(index, iterLength);

        if (iterDims.length > 0) {
            const iterSize = iterDims.reduce((a, b) => a * b);
            return this._subarray(index, iterSize, iterDims);
        } else {
            return new Tensor(this.type, [this.data[index]], iterDims);
        }
    }

    /**
     * @param {number|bigint} item The item to search for in the tensor
     * @returns {number} The index of the first occurrence of item in the tensor data.
     */
    indexOf(item) {
        const this_data = this.data;
        for (let index = 0; index < this_data.length; ++index) {
            // Note: == instead of === so we can match Ints with BigInts
            if (this_data[index] == item) {
                return index;
            }
        }
        return -1;
    }

    /**
     * @param {number} index
     * @param {number} iterSize
     * @param {any} iterDims
     * @returns {Tensor}
     */
    _subarray(index, iterSize, iterDims) {
        const o1 = index * iterSize;
        const o2 = (index + 1) * iterSize;

        // We use subarray if available (typed array), otherwise we use slice (normal array)
        const data = 'subarray' in this.data ? this.data.subarray(o1, o2) : this.data.slice(o1, o2);
        return new Tensor(this.type, data, iterDims);
    }

    /**
     * Returns the value of this tensor as a standard JavaScript Number. This only works
     * for tensors with one element. For other cases, see `Tensor.tolist()`.
     * @returns {number|bigint} The value of this tensor as a standard JavaScript Number.
     * @throws {Error} If the tensor has more than one element.
     */
    item() {
        const this_data = this.data;
        if (this_data.length !== 1) {
            throw new Error(`a Tensor with ${this_data.length} elements cannot be converted to Scalar`);
        }
        return this_data[0];
    }

    /**
     * Convert tensor data to a n-dimensional JS list
     * @returns {any[]}
     */
    tolist() {
        return reshape(this.data, this.dims);
    }

    /**
     * Return a new Tensor with the sigmoid function applied to each element.
     * @returns {Tensor} The tensor with the sigmoid function applied.
     */
    sigmoid() {
        return this.clone().sigmoid_();
    }

    /**
     * Applies the sigmoid function to the tensor in place.
     * @returns {Tensor} Returns `this`.
     */
    sigmoid_() {
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            this_data[i] = 1 / (1 + Math.exp(-this_data[i]));
        }
        return this;
    }

    /**
     * Return a new Tensor with a callback function applied to each element.
     * @param {Function} callback - The function to apply to each element. It should take three arguments:
     *                              the current element, its index, and the tensor's data array.
     * @returns {Tensor} A new Tensor with the callback function applied to each element.
     */
    map(callback) {
        return this.clone().map_(callback);
    }

    /**
     * Apply a callback function to each element of the tensor in place.
     * @param {Function} callback - The function to apply to each element. It should take three arguments:
     *                              the current element, its index, and the tensor's data array.
     * @returns {Tensor} Returns `this`.
     */
    map_(callback) {
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            this_data[i] = callback(this_data[i], i, this_data);
        }
        return this;
    }

    /**
     * Return a new Tensor with every element multiplied by a constant.
     * @param {number} val The value to multiply by.
     * @returns {Tensor} The new tensor.
     */
    mul(val) {
        return this.clone().mul_(val);
    }

    /**
     * Multiply the tensor by a constant in place.
     * @param {number} val The value to multiply by.
     * @returns {Tensor} Returns `this`.
     */
    mul_(val) {
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            this_data[i] *= val;
        }
        return this;
    }

    /**
     * Return a new Tensor with every element divided by a constant.
     * @param {number} val The value to divide by.
     * @returns {Tensor} The new tensor.
     */
    div(val) {
        return this.clone().div_(val);
    }

    /**
     * Divide the tensor by a constant in place.
     * @param {number} val The value to divide by.
     * @returns {Tensor} Returns `this`.
     */
    div_(val) {
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            this_data[i] /= val;
        }
        return this;
    }

    /**
     * Return a new Tensor with every element added by a constant.
     * @param {number} val The value to add by.
     * @returns {Tensor} The new tensor.
     */
    add(val) {
        return this.clone().add_(val);
    }

    /**
     * Add the tensor by a constant in place.
     * @param {number} val The value to add by.
     * @returns {Tensor} Returns `this`.
     */
    add_(val) {
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            this_data[i] += val;
        }
        return this;
    }

    /**
     * Return a new Tensor with every element subtracted by a constant.
     * @param {number} val The value to subtract by.
     * @returns {Tensor} The new tensor.
     */
    sub(val) {
        return this.clone().sub_(val);
    }

    /**
     * Subtract the tensor by a constant in place.
     * @param {number} val The value to subtract by.
     * @returns {Tensor} Returns `this`.
     */
    sub_(val) {
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            this_data[i] -= val;
        }
        return this;
    }

    /**
     * Creates a deep copy of the current Tensor.
     * @returns {Tensor} A new Tensor with the same type, data, and dimensions as the original.
     */
    clone() {
        return new Tensor(this.type, this.data.slice(), this.dims.slice());
    }

    /**
     * Performs a slice operation on the Tensor along specified dimensions.
     *
     * Consider a Tensor that has a dimension of [4, 7]:
     * ```
     * [ 1,  2,  3,  4,  5,  6,  7]
     * [ 8,  9, 10, 11, 12, 13, 14]
     * [15, 16, 17, 18, 19, 20, 21]
     * [22, 23, 24, 25, 26, 27, 28]
     * ```
     * We can slice against the two dims of row and column, for instance in this
     * case we can start at the second element, and return to the second last,
     * like this:
     * ```
     * tensor.slice([1, -1], [1, -1]);
     * ```
     * which would return:
     * ```
     * [  9, 10, 11, 12, 13 ]
     * [ 16, 17, 18, 19, 20 ]
     * ```
     *
     * @param {...(number|number[]|null)} slices The slice specifications for each dimension.
     * - If a number is given, then a single element is selected.
     * - If an array of two numbers is given, then a range of elements [start, end (exclusive)] is selected.
     * - If null is given, then the entire dimension is selected.
     * @returns {Tensor} A new Tensor containing the selected elements.
     * @throws {Error} If the slice input is invalid.
     */
    slice(...slices) {
        // This allows for slicing with ranges and numbers
        const newTensorDims = [];
        const newOffsets = [];

        // slices is an array of numbers or arrays of numbers
        // e.g., slices = [0, [1, 3], null, [0, 3]]
        for (let sliceIndex = 0; sliceIndex < this.dims.length; ++sliceIndex) {
            let slice = slices[sliceIndex];

            if (slice === null || slice === undefined) {
                // null or undefined means take the whole dimension
                newOffsets.push([0, this.dims[sliceIndex]]);
                newTensorDims.push(this.dims[sliceIndex]);
            } else if (typeof slice === 'number') {
                slice = safeIndex(slice, this.dims[sliceIndex], sliceIndex);

                // A number means take a single element
                newOffsets.push([slice, slice + 1]);
            } else if (Array.isArray(slice) && slice.length === 2) {
                // An array of length 2 means take a range of elements
                let [start, end] = slice;
                start = start === null ? 0 : safeIndex(start, this.dims[sliceIndex], sliceIndex, false);
                end = end === null ? this.dims[sliceIndex] : safeIndex(end, this.dims[sliceIndex], sliceIndex, false);

                if (start > end) {
                    throw new Error(`Invalid slice: ${slice}`);
                }

                const offsets = [Math.max(start, 0), Math.min(end, this.dims[sliceIndex])];

                newOffsets.push(offsets);
                newTensorDims.push(offsets[1] - offsets[0]);
            } else {
                throw new Error(`Invalid slice: ${slice}`);
            }
        }

        const newDims = newOffsets.map(([start, end]) => end - start);
        const newBufferSize = newDims.reduce((a, b) => a * b);

        const this_data = this.data;
        // Allocate memory
        // @ts-ignore
        const data = new this_data.constructor(newBufferSize);

        // Precompute strides
        const stride = this.stride();

        // Detect if the slice is contiguous
        let isContiguous = true;
        for (let i = 1; i < newDims.length; ++i) {
            if (newOffsets[i][0] !== 0 || newOffsets[i][1] !== this.dims[i]) {
                isContiguous = false;
                break;
            }
        }

        if (isContiguous) {
            // Perform bulk copy for contiguous slices to improve performance
            const start = newOffsets[0][0] * stride[0];
            const end = newOffsets[0][1] * stride[0];

            if (ArrayBuffer.isView(this_data)) {
                // If this.data is a TypedArray, use subarray
                // @ts-ignore
                data.set(this_data.subarray(start, end));
            } else if (Array.isArray(this_data)) {
                // If this.data is a plain array, use slice
                const slicedData = this_data.slice(start, end);
                for (let i = 0; i < slicedData.length; ++i) {
                    data[i] = slicedData[i];
                }
            } else {
                throw new Error('Unsupported data type for slicing');
            }
        } else {
            // Fallback to manual copying for non-contiguous slices
            for (let i = 0; i < newBufferSize; ++i) {
                let originalIndex = 0;
                for (let j = newDims.length - 1, num = i; j >= 0; --j) {
                    const size = newDims[j];
                    originalIndex += ((num % size) + newOffsets[j][0]) * stride[j];
                    num = Math.floor(num / size);
                }
                data[i] = this_data[originalIndex];
            }
        }

        return new Tensor(this.type, data, newTensorDims);
    }

    /**
     * Return a permuted version of this Tensor, according to the provided dimensions.
     * @param  {...number} dims Dimensions to permute.
     * @returns {Tensor} The permuted tensor.
     */
    permute(...dims) {
        return permute(this, dims);
    }

    // TODO: implement transpose. For now (backwards compatibility), it's just an alias for permute()
    /** @type {Tensor['permute']} */
    transpose(...dims) {
        return this.permute(...dims);
    }

    /**
     * Returns the sum of each row of the input tensor in the given dimension dim.
     *
     * @param {number|null} [dim=null] The dimension or dimensions to reduce. If `null`, all dimensions are reduced.
     * @param {boolean} keepdim Whether the output tensor has `dim` retained or not.
     * @returns The summed tensor
     */
    sum(dim = null, keepdim = false) {
        return this.norm(1, dim, keepdim);
    }

    /**
     * Returns the matrix norm or vector norm of a given tensor.
     * @param {number|string} [p='fro'] The order of norm
     * @param {number|null} [dim=null] Specifies which dimension of the tensor to calculate the norm across.
     * If dim is None, the norm will be calculated across all dimensions of input.
     * @param {boolean} [keepdim=false] Whether the output tensors have dim retained or not.
     * @returns {Tensor} The norm of the tensor.
     */
    norm(p = 'fro', dim = null, keepdim = false) {
        if (p === 'fro') {
            // NOTE: Since we only support integer dims, Frobenius norm produces the same result as p=2.
            p = 2;
        } else if (typeof p === 'string') {
            throw Error(`Unsupported norm: ${p}`);
        }

        const this_data = this.data;
        const is_bigint = this_data instanceof BigInt64Array || this_data instanceof BigUint64Array;

        if (is_bigint && p !== 1) {
            throw Error(`Expected a floating point tensor as input. Got ${this.type}`);
        }

        let fn, zero;
        if (is_bigint) {
            fn = (/** @type {bigint} */ a, /** @type {bigint} */ b) => a + b;
            zero = 0n;
        } else {
            fn = (/** @type {number} */ a, /** @type {number} */ b) => a + b ** p;
            zero = 0;
        }

        if (dim === null) {
            // @ts-ignore
            let val = this_data.reduce(fn, zero);
            if (p !== 1) {
                val = val ** (1 / p);
            }
            return new Tensor(this.type, [val], []);
        }

        const [type, result, resultDims] = reduce_helper(fn, this, dim, keepdim);

        if (p !== 1) {
            for (let i = 0; i < result.length; ++i) {
                result[i] = result[i] ** (1 / p);
            }
        }
        return new Tensor(type, result, resultDims);
    }

    /**
     * Performs `L_p` normalization of inputs over specified dimension. Operates in place.
     * @param {number} [p=2] The exponent value in the norm formulation
     * @param {number} [dim=1] The dimension to reduce
     * @returns {Tensor} `this` for operation chaining.
     */
    normalize_(p = 2.0, dim = 1) {
        dim = safeIndex(dim, this.dims.length);

        const norm = this.norm(p, dim, true);

        const this_data = this.data;
        const norm_data = norm.data;
        for (let i = 0; i < this_data.length; ++i) {
            // Calculate the index in the resulting array
            let resultIndex = 0;

            for (let j = this.dims.length - 1, num = i, resultMultiplier = 1; j >= 0; --j) {
                const size = this.dims[j];
                if (j !== dim) {
                    const index = num % size;
                    resultIndex += index * resultMultiplier;
                    resultMultiplier *= this.dims[j];
                }
                num = Math.floor(num / size);
            }

            // Divide by normalized value
            this_data[i] /= norm_data[resultIndex];
        }

        return this;
    }

    /**
     * Performs `L_p` normalization of inputs over specified dimension.
     * @param {number} [p=2] The exponent value in the norm formulation
     * @param {number} [dim=1] The dimension to reduce
     * @returns {Tensor} The normalized tensor.
     */
    normalize(p = 2.0, dim = 1) {
        return this.clone().normalize_(p, dim);
    }

    /**
     * Compute and return the stride of this tensor.
     * Stride is the jump necessary to go from one element to the next one in the specified dimension dim.
     * @returns {number[]} The stride of this tensor.
     */
    stride() {
        return dimsToStride(this.dims);
    }

    /**
     * Returns a tensor with all specified dimensions of input of size 1 removed.
     *
     * NOTE: The returned tensor shares the storage with the input tensor, so changing the contents of one will change the contents of the other.
     * If you would like a copy, use `tensor.clone()` before squeezing.
     *
     * @param {number|number[]|null} [dim=null] If given, the input will be squeezed only in the specified dimensions.
     * @returns {Tensor} The squeezed tensor
     */
    squeeze(dim = null) {
        return new Tensor(this.type, this.data, calc_squeeze_dims(this.dims, dim));
    }

    /**
     * In-place version of @see {@link Tensor.squeeze}
     */
    squeeze_(dim = null) {
        this.dims = calc_squeeze_dims(this.dims, dim);
        return this;
    }

    /**
     * Returns a new tensor with a dimension of size one inserted at the specified position.
     *
     * NOTE: The returned tensor shares the same underlying data with this tensor.
     *
     * @param {number} dim The index at which to insert the singleton dimension
     * @returns {Tensor} The unsqueezed tensor
     */
    unsqueeze(dim) {
        return new Tensor(this.type, this.data, calc_unsqueeze_dims(this.dims, dim));
    }

    /**
     * In-place version of @see {@link Tensor.unsqueeze}
     * @type {Tensor['unsqueeze']}
     */
    unsqueeze_(dim) {
        this.dims = calc_unsqueeze_dims(this.dims, dim);
        return this;
    }

    /**
     * In-place version of @see {@link Tensor.flatten}
     */
    flatten_(start_dim = 0, end_dim = -1) {
        // TODO validate inputs
        end_dim = (end_dim + this.dims.length) % this.dims.length;

        let dimsToKeepBefore = this.dims.slice(0, start_dim);
        let dimsToFlatten = this.dims.slice(start_dim, end_dim + 1);
        let dimsToKeepAfter = this.dims.slice(end_dim + 1);

        this.dims = [...dimsToKeepBefore, dimsToFlatten.reduce((a, b) => a * b, 1), ...dimsToKeepAfter];
        return this;
    }

    /**
     * Flattens input by reshaping it into a one-dimensional tensor.
     * If `start_dim` or `end_dim` are passed, only dimensions starting with `start_dim`
     * and ending with `end_dim` are flattened. The order of elements in input is unchanged.
     * @param {number} start_dim the first dim to flatten
     * @param {number} end_dim the last dim to flatten
     * @returns {Tensor} The flattened tensor.
     */
    flatten(start_dim = 0, end_dim = -1) {
        return this.clone().flatten_(start_dim, end_dim);
    }

    /**
     * Returns a new tensor with the same data as the `self` tensor but of a different `shape`.
     * @param  {...number} dims the desired size
     * @returns {Tensor} The tensor with the same data but different shape
     */
    view(...dims) {
        // TODO: validate dims
        let inferredIndex = -1;
        for (let i = 0; i < dims.length; ++i) {
            if (dims[i] === -1) {
                if (inferredIndex !== -1) {
                    throw new Error('Only one dimension can be inferred');
                }
                inferredIndex = i;
            }
        }

        const this_data = this.data;
        if (inferredIndex !== -1) {
            // Some dimension must be inferred
            const productOther = dims.reduce((product, curr, index) => {
                return index !== inferredIndex ? product * curr : product;
            }, 1);

            dims[inferredIndex] = this_data.length / productOther;
        }
        return new Tensor(this.type, this_data, dims); // NOTE: uses same underlying storage
    }

    neg_() {
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            this_data[i] = -this_data[i];
        }
        return this;
    }
    neg() {
        return this.clone().neg_();
    }

    /**
     * Computes input > val element-wise.
     * @param {number} val The value to compare with.
     * @returns {Tensor} A boolean tensor that is `true` where input is greater than other and `false` elsewhere.
     */
    gt(val) {
        const mask = new Uint8Array(this.data.length);
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            mask[i] = this_data[i] > val ? 1 : 0;
        }
        return new Tensor('bool', mask, this.dims);
    }

    /**
     * Computes input < val element-wise.
     * @param {number} val The value to compare with.
     * @returns {Tensor} A boolean tensor that is `true` where input is less than other and `false` elsewhere.
     */
    lt(val) {
        const mask = new Uint8Array(this.data.length);
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            mask[i] = this_data[i] < val ? 1 : 0;
        }
        return new Tensor('bool', mask, this.dims);
    }

    /**
     * In-place version of @see {@link Tensor.clamp}
     * @type {Tensor['clamp']}
     */
    clamp_(min, max) {
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            this_data[i] = Math.min(Math.max(this_data[i], min), max);
        }
        return this;
    }

    /**
     * Clamps all elements in input into the range [ min, max ]
     * @param {number} min lower-bound of the range to be clamped to
     * @param {number} max upper-bound of the range to be clamped to
     * @returns {Tensor} the output tensor.
     */
    clamp(min, max) {
        return this.clone().clamp_(min, max);
    }

    /**
     * In-place version of @see {@link Tensor.round}
     */
    round_() {
        const this_data = this.data;
        for (let i = 0; i < this_data.length; ++i) {
            this_data[i] = Math.round(this_data[i]);
        }
        return this;
    }

    /**
     * Rounds elements of input to the nearest integer.
     * @returns {Tensor} the output tensor.
     */
    round() {
        return this.clone().round_();
    }

    mean(dim = null, keepdim = false) {
        return mean(this, dim, keepdim);
    }

    min(dim = null, keepdim = false) {
        if (dim === null) {
            // None to reduce over all dimensions.
            const val = min(this.data)[0];
            return new Tensor(
                this.type,
                [val],
                [
                    /* scalar */
                ],
            );
        }
        const [type, result, resultDims] = reduce_helper((a, b) => Math.min(a, b), this, dim, keepdim, Infinity);
        return new Tensor(type, result, resultDims);
    }

    max(dim = null, keepdim = false) {
        if (dim === null) {
            // None to reduce over all dimensions.
            const val = max(this.data)[0];
            return new Tensor(
                this.type,
                [val],
                [
                    /* scalar */
                ],
            );
        }
        const [type, result, resultDims] = reduce_helper((a, b) => Math.max(a, b), this, dim, keepdim, -Infinity);
        return new Tensor(type, result, resultDims);
    }

    argmin(dim = null, keepdim = false) {
        if (dim !== null) {
            throw new Error('`dim !== null` not yet implemented.');
        }
        const index = min(this.data)[1];
        return new Tensor('int64', [BigInt(index)], []);
    }
    argmax(dim = null, keepdim = false) {
        if (dim !== null) {
            throw new Error('`dim !== null` not yet implemented.');
        }
        const index = max(this.data)[1];
        return new Tensor('int64', [BigInt(index)], []);
    }

    /**
     * Repeats this tensor along the specified dimensions.
     * @param {...number} repeats The number of times to repeat this tensor along each dimension.
     * @returns {Tensor} The repeated tensor.
     * @throws {Error} If the number of repeats is less than the number of dimensions.
     */
    repeat(...repeats) {
        if (repeats.length < this.dims.length) {
            throw new Error(
                `Number of dimensions of repeat dims (${repeats.length}) cannot be smaller than number of dimensions of tensor (${this.dims.length})`,
            );
        }

        // Optimization: if all repeats are 1, just clone with potentially expanded dims
        if (repeats.every((r) => r === 1)) {
            if (repeats.length === this.dims.length) {
                return this.clone();
            }
            // Need to expand dimensions by prepending 1s
            const numPrependedDims = repeats.length - this.dims.length;
            const newDims = Array(numPrependedDims).fill(1).concat(this.dims);
            return new Tensor(this.type, this.data.slice(), newDims);
        }

        // If repeats has more dimensions than the tensor, prepend 1s to tensor dims
        const numPrependedDims = repeats.length - this.dims.length;
        const expandedDims = Array(numPrependedDims).fill(1).concat(this.dims);

        // Calculate the output dimensions
        const outputDims = expandedDims.map((dim, i) => dim * repeats[i]);

        // Calculate total output size
        const outputSize = outputDims.reduce((a, b) => a * b, 1);

        // Allocate output data
        const this_data = this.data;
        // @ts-ignore
        const data = new this_data.constructor(outputSize);

        // Calculate strides for the expanded input tensor
        const inputStrides = dimsToStride(expandedDims);
        const outputStrides = dimsToStride(outputDims);

        // Fill the output tensor
        for (let i = 0; i < outputSize; ++i) {
            // Convert flat index to multi-dimensional indices in output
            let remaining = i;
            let inputIndex = 0;

            for (let d = 0; d < outputDims.length; ++d) {
                const outputCoord = Math.floor(remaining / outputStrides[d]);
                remaining = remaining % outputStrides[d];

                // Map output coordinate to input coordinate using modulo
                const inputCoord = outputCoord % expandedDims[d];
                inputIndex += inputCoord * inputStrides[d];
            }

            data[i] = this_data[inputIndex];
        }

        return new Tensor(this.type, data, outputDims);
    }

    /**
     * Constructs a tensor by repeating the elements of input. The dims argument specifies the number of repetitions in each dimension.
     * @param  {...number} dims The number of repetitions per dimension.
     * @returns {Tensor} The tiled tensor.
     */
    tile(...dims) {
        // If fewer repeats than dims, prepend 1s to repeats
        if (dims.length < this.dims.length) {
            const numPrependedRepeats = this.dims.length - dims.length;
            dims = Array(numPrependedRepeats).fill(1).concat(dims);
        }
        return this.repeat(...dims);
    }

    /**
     * Performs Tensor dtype conversion.
     * @param {DataType} type The desired data type.
     * @returns {Tensor} The converted tensor.
     */
    to(type) {
        // If the self Tensor already has the correct dtype, then self is returned.
        if (this.type === type) return this;

        // Otherwise, the returned tensor is a copy of self with the desired dtype.
        if (!DataTypeMap.hasOwnProperty(type)) {
            throw new Error(`Unsupported type: ${type}`);
        }

        // Handle special cases where a mapping function is needed (e.g., where one type is a bigint and the other is a number)
        let map_fn;
        const is_source_bigint = ['int64', 'uint64'].includes(this.type);
        const is_dest_bigint = ['int64', 'uint64'].includes(type);
        if (is_source_bigint && !is_dest_bigint) {
            // TypeError: Cannot convert a BigInt value to a number
            map_fn = Number;
        } else if (!is_source_bigint && is_dest_bigint) {
            // TypeError: Cannot convert [x] to a BigInt
            if (['float16', 'float32', 'float64'].includes(this.type)) {
                map_fn = (x) => BigInt(Math.floor(x));
            } else {
                map_fn = BigInt;
            }
        } else if (this.type === 'float16' && type == 'float32' && this.data instanceof Uint16Array) {
            // Certain runtimes do not support Float16Array, so the values are stored in Uint16Array
            return new Tensor(type, uint16_to_float32(this.data), this.dims);
        }

        return new Tensor(
            type,
            // @ts-ignore - TypeScript can't verify .from() across union of TypedArray constructors
            DataTypeMap[type].from(this.data, map_fn),
            this.dims,
        );
    }
}

/**
 * This creates a nested array of a given type and depth (see examples).
 *
 * @example
 *   NestArray<string, 1>; // string[]
 * @example
 *   NestArray<number, 2>; // number[][]
 * @example
 *   NestArray<string, 3>; // string[][][] etc.
 * @template T
 * @template {number} Depth
 * @template {never[]} [Acc=[]]
 * @typedef {Acc['length'] extends Depth ? T : NestArray<T[], Depth, [...Acc, never]>} NestArray
 */

/**
 * Reshapes a 1-dimensional array into an n-dimensional array, according to the provided dimensions.
 *
 * @example
 *   reshape([10                    ], [1      ]); // Type: number[]      Value: [10]
 *   reshape([1, 2, 3, 4            ], [2, 2   ]); // Type: number[][]    Value: [[1, 2], [3, 4]]
 *   reshape([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); // Type: number[][][]  Value: [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
 *   reshape([1, 2, 3, 4, 5, 6, 7, 8], [4, 2   ]); // Type: number[][]    Value: [[1, 2], [3, 4], [5, 6], [7, 8]]
 * @param {T[]|DataArray} data The input array to reshape.
 * @param {DIM} dimensions The target shape/dimensions.
 * @template T
 * @template {[number]|number[]} DIM
 * @returns {NestArray<T, DIM["length"]>} The reshaped array.
 */
function reshape(data, dimensions) {
    const totalElements = data.length;
    const dimensionSize = dimensions.reduce((a, b) => a * b);

    if (totalElements !== dimensionSize) {
        throw Error(`cannot reshape array of size ${totalElements} into shape (${dimensions})`);
    }

    /** @type {any} */
    let reshapedArray = data;

    for (let i = dimensions.length - 1; i >= 0; i--) {
        reshapedArray = reshapedArray.reduce(
            (acc, val) => {
                let lastArray = acc[acc.length - 1];

                if (lastArray.length < dimensions[i]) {
                    lastArray.push(val);
                } else {
                    acc.push([val]);
                }

                return acc;
            },
            [[]],
        );
    }

    return reshapedArray[0];
}

/**
 * Permutes a tensor according to the provided axes.
 * @param {any} tensor The input tensor to permute.
 * @param {number[]} axes The axes to permute the tensor along.
 * @returns {Tensor} The permuted tensor.
 */
export function permute(tensor, axes) {
    const [permutedData, shape] = permute_data(tensor.data, tensor.dims, axes);
    return new Tensor(tensor.type, permutedData, shape);
}

/**
 * Interpolates an Tensor to the given size.
 * @param {Tensor} input The input tensor to interpolate. Data must be channel-first (i.e., [c, h, w])
 * @param {number[]} size The output size of the image
 * @param {string} mode The interpolation mode
 * @param {boolean} align_corners Whether to align corners.
 * @returns {Tensor} The interpolated tensor.
 */
export function interpolate(input, [out_height, out_width], mode = 'bilinear', align_corners = false) {
    // Input image dimensions
    const in_channels = input.dims.at(-3) ?? 1;
    const in_height = input.dims.at(-2);
    const in_width = input.dims.at(-1);

    const output = interpolate_data(
        /** @type {import('./maths.js').TypedArray}*/ (input.data),
        [in_channels, in_height, in_width],
        [out_height, out_width],
        mode,
        align_corners,
    );
    return new Tensor(input.type, output, [in_channels, out_height, out_width]);
}

/**
 * Down/up samples the input.
 * Inspired by https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html.
 * @param {Tensor} input the input tensor
 * @param {Object} options the options for the interpolation
 * @param {[number, number]|[number, number, number]|[number, number, number, number]} [options.size=null] output spatial size.
 * @param {"nearest"|"bilinear"|"bicubic"} [options.mode='bilinear'] algorithm used for upsampling
 * @returns {Promise<Tensor>} The interpolated tensor.
 */
export async function interpolate_4d(input, { size = null, mode = 'bilinear' } = {}) {
    // Error checking
    if (input.dims.length !== 4) {
        throw new Error('`interpolate_4d` currently only supports 4D input.');
    }
    if (!size) {
        // TODO: support scale_factor
        throw new Error('`interpolate_4d` requires a `size` argument.');
    }

    // Fill in missing dimensions
    let targetDims;
    if (size.length === 2) {
        targetDims = [...input.dims.slice(0, 2), ...size];
    } else if (size.length === 3) {
        targetDims = [input.dims[0], ...size];
    } else if (size.length === 4) {
        targetDims = size;
    } else {
        throw new Error('`size` must be of length 2, 3, or 4.');
    }

    let op;
    if (mode === 'nearest') {
        op = await TensorOpRegistry.nearest_interpolate_4d;
    } else if (mode === 'bilinear') {
        op = await TensorOpRegistry.bilinear_interpolate_4d;
    } else if (mode === 'bicubic') {
        op = await TensorOpRegistry.bicubic_interpolate_4d;
    } else {
        throw new Error(`Unsupported mode: ${mode}`);
    }

    const sizeTensor = new Tensor('int64', new BigInt64Array(targetDims.map(BigInt)), [targetDims.length]);
    return await op({ x: input, s: sizeTensor });
}

/**
 * Matrix product of two tensors.
 * Inspired by https://pytorch.org/docs/stable/generated/torch.matmul.html
 * @param {Tensor} a the first tensor to be multiplied
 * @param {Tensor} b the second tensor to be multiplied
 * @returns {Promise<Tensor>} The matrix product of the two tensors.
 */
export async function matmul(a, b) {
    const op = await TensorOpRegistry.matmul;
    return await op({ a, b });
}

/**
 * Computes the one dimensional Fourier transform of real-valued input.
 * Inspired by https://pytorch.org/docs/stable/generated/torch.fft.rfft.html
 * @param {Tensor} x the real input tensor
 * @param {Tensor} a The dimension along which to take the one dimensional real FFT.
 * @returns {Promise<Tensor>} the output tensor.
 */
export async function rfft(x, a) {
    const op = await TensorOpRegistry.rfft;
    return await op({ x, a });
}

/**
 * Returns the k largest elements of the given input tensor.
 * Inspired by https://pytorch.org/docs/stable/generated/torch.topk.html
 * @param {Tensor} x the input tensor
 * @param {number} [k] the k in "top-k"
 * @returns {Promise<[Tensor, Tensor]>} the output tuple of (Tensor, LongTensor) of top-k elements and their indices.
 */
export async function topk(x, k) {
    const op = await TensorOpRegistry.top_k;

    if (k == null) {
        k = x.dims.at(-1);
    } else {
        k = Math.min(k, x.dims.at(-1));
    }
    return await op({
        x,
        k: new Tensor('int64', [BigInt(k)], [1]),
    });
}

const arrayToIndexTensor = (array) => new Tensor('int64', array, [array.length]);
/**
 * Slice a multidimensional float32 tensor.
 * @param {Tensor} data: Tensor of data to extract slices from
 * @param {number[]} starts: 1-D array of starting indices of corresponding axis in axes
 * @param {number[]} ends: 1-D array of ending indices (exclusive) of corresponding axis in axes
 * @param {number[]} axes: 1-D array of axes that starts and ends apply to
 * @param {number[]} [steps]: 1-D array of slice step of corresponding axis in axes.
 * @returns {Promise<Tensor>} Sliced data tensor.
 */
export async function slice(data, starts, ends, axes, steps) {
    const op = await TensorOpRegistry.slice;
    return await op({
        x: data,
        s: arrayToIndexTensor(starts),
        e: arrayToIndexTensor(ends),
        a: arrayToIndexTensor(axes),
        t: arrayToIndexTensor(steps ?? new Array(axes.length).fill(1)),
    });
}

/**
 * Perform mean pooling of the last hidden state followed by a normalization step.
 * @param {Tensor} last_hidden_state Tensor of shape [batchSize, seqLength, embedDim]
 * @param {Tensor} attention_mask Tensor of shape [batchSize, seqLength]
 * @returns {Tensor} Returns a new Tensor of shape [batchSize, embedDim].
 */
export function mean_pooling(last_hidden_state, attention_mask) {
    // last_hidden_state: [batchSize, seqLength, embedDim]
    // attention_mask:    [batchSize, seqLength]
    const lastHiddenStateData = last_hidden_state.data;
    const attentionMaskData = attention_mask.data;

    const shape = [last_hidden_state.dims[0], last_hidden_state.dims[2]];

    // @ts-ignore
    const returnedData = new lastHiddenStateData.constructor(shape[0] * shape[1]);
    const [batchSize, seqLength, embedDim] = last_hidden_state.dims;

    let outIndex = 0;
    for (let i = 0; i < batchSize; ++i) {
        const offset = i * embedDim * seqLength;

        for (let k = 0; k < embedDim; ++k) {
            let sum = 0;
            let count = 0;

            const attnMaskOffset = i * seqLength;
            const offset2 = offset + k;
            // Pool over all words in sequence
            for (let j = 0; j < seqLength; ++j) {
                // index into attention mask
                const attn = Number(attentionMaskData[attnMaskOffset + j]);

                count += attn;
                sum += lastHiddenStateData[offset2 + j * embedDim] * attn;
            }

            const avg = sum / count;
            returnedData[outIndex++] = avg;
        }
    }

    return new Tensor(last_hidden_state.type, returnedData, shape);
}

/**
 * Apply Layer Normalization for last certain number of dimensions.
 * @param {Tensor} input The input tensor
 * @param {number[]} normalized_shape input shape from an expected input of size
 * @param {Object} options The options for the layer normalization
 * @param {number} [options.eps=1e-5] A value added to the denominator for numerical stability.
 * @returns {Tensor} The normalized tensor.
 */
export function layer_norm(input, normalized_shape, { eps = 1e-5 } = {}) {
    if (input.dims.length !== 2) {
        throw new Error('`layer_norm` currently only supports 2D input.');
    }

    const [batchSize, featureDim] = input.dims;

    if (normalized_shape.length !== 1 && normalized_shape[0] !== featureDim) {
        throw new Error('`normalized_shape` must be a 1D array with shape `[input.dims[1]]`.');
    }

    const [std, mean] = std_mean(input, 1, 0, true);
    const stdData = /** @type {Float32Array} */ (std.data);
    const meanData = /** @type {Float32Array} */ (mean.data);

    const inputData = /** @type {Float32Array} */ (input.data);

    // @ts-ignore
    const returnedData = new inputData.constructor(inputData.length);

    for (let i = 0; i < batchSize; ++i) {
        const offset = i * featureDim;
        for (let j = 0; j < featureDim; ++j) {
            const offset2 = offset + j;
            returnedData[offset2] = (inputData[offset2] - meanData[i]) / (stdData[i] + eps);
        }
    }
    return new Tensor(input.type, returnedData, input.dims);
}

/**
 * Helper function to calculate new dimensions when performing a squeeze operation.
 * @param {number[]} dims The dimensions of the tensor.
 * @param {number|number[]|null} dim The dimension(s) to squeeze.
 * @returns {number[]} The new dimensions.
 * @private
 */
function calc_squeeze_dims(dims, dim) {
    dims = dims.slice();
    if (dim === null) {
        dims = dims.filter((d) => d !== 1);
    } else if (typeof dim === 'number') {
        if (dims[dim] === 1) {
            dims.splice(dim, 1);
        }
    } else if (Array.isArray(dim)) {
        dims = dims.filter((x, i) => {
            return x !== 1 || !dim.includes(i);
        });
    }
    return dims;
}

/**
 * Helper function to calculate new dimensions when performing an unsqueeze operation.
 * @param {number[]} dims The dimensions of the tensor.
 * @param {number} dim The dimension to unsqueeze.
 * @returns {number[]} The new dimensions.
 * @private
 */
function calc_unsqueeze_dims(dims, dim) {
    // Dimension out of range (e.g., "expected to be in range of [-4, 3], but got 4")
    // + 1 since we allow inserting at the end (i.e. dim = -1)
    dim = safeIndex(dim, dims.length + 1);
    dims = dims.slice();
    // Insert 1 into specified dimension
    dims.splice(dim, 0, 1);
    return dims;
}

/**
 * Safely calculate the index for an array of a given size, allowing negative indexing.
 * @param {number} index The index that will be used.
 * @param {number} size The size of the array.
 * @param {number|null} [dimension=null] The dimension that the index is for (optional).
 * @returns {number} The index, guaranteed to be non-negative and less than `arrayLength`.
 *
 * @throws {Error} If the index is out of range.
 * @private
 */
function safeIndex(index, size, dimension = null, boundsCheck = true) {
    if (index < -size || index >= size) {
        if (boundsCheck) {
            throw new Error(
                `IndexError: index ${index} is out of bounds for dimension${dimension === null ? '' : ' ' + dimension} with size ${size}`,
            );
        } else {
            return index < -size ? 0 : size;
        }
    }

    if (index < 0) {
        // Negative indexing, ensuring positive index
        index = ((index % size) + size) % size;
    }
    return index;
}

/**
 * Concatenates an array of tensors along a specified dimension.
 * @param {Tensor[]} tensors The array of tensors to concatenate.
 * @param {number} dim The dimension to concatenate along.
 * @returns {Tensor} The concatenated tensor.
 */
export function cat(tensors, dim = 0) {
    dim = safeIndex(dim, tensors[0].dims.length);

    // TODO do validation of shapes

    const resultDims = tensors[0].dims.slice();
    resultDims[dim] = tensors.reduce((a, b) => a + b.dims[dim], 0);

    // Create a new array to store the accumulated values
    const resultSize = resultDims.reduce((a, b) => a * b, 1);
    // @ts-ignore
    const result = new tensors[0].data.constructor(resultSize);

    // Create output tensor of same type as first
    const resultType = tensors[0].type;

    if (dim === 0) {
        // Handle special case for performance reasons

        let offset = 0;
        for (const tensor of tensors) {
            const tensorData = tensor.data;
            result.set(tensorData, offset);
            offset += tensorData.length;
        }
    } else {
        let currentDim = 0;

        for (let t = 0; t < tensors.length; ++t) {
            const { data, dims } = tensors[t];

            // Iterate over the data array
            for (let i = 0; i < data.length; ++i) {
                // Calculate the index in the resulting array
                let resultIndex = 0;

                for (let j = dims.length - 1, num = i, resultMultiplier = 1; j >= 0; --j) {
                    const size = dims[j];
                    let index = num % size;
                    if (j === dim) {
                        index += currentDim;
                    }
                    resultIndex += index * resultMultiplier;
                    resultMultiplier *= resultDims[j];
                    num = Math.floor(num / size);
                }
                // Accumulate the value at the current index
                result[resultIndex] = data[i];
            }

            currentDim += dims[dim];
        }
    }
    return new Tensor(resultType, result, resultDims);
}

/**
 * Stack an array of tensors along a specified dimension.
 * @param {Tensor[]} tensors The array of tensors to stack.
 * @param {number} dim The dimension to stack along.
 * @returns {Tensor} The stacked tensor.
 */
export function stack(tensors, dim = 0) {
    // TODO do validation of shapes
    // NOTE: stack expects each tensor to be equal size
    return cat(
        tensors.map((t) => t.unsqueeze(dim)),
        dim,
    );
}

/**
 * @param {(previousValue: any, currentValue: any, currentIndex?: number, resultIndex?: number) => any} callbackfn
 * @param {Tensor} input the input tensor.
 * @param {number} dim the dimension to reduce.
 * @param {boolean} keepdim whether the output tensor has dim retained or not.
 * @param {any} [initialValue=null] the initial value to start the reduction with.
 * @returns {[DataType, any, number[]]} The reduced tensor data.
 */
function reduce_helper(callbackfn, input, dim, keepdim = false, initialValue = null) {
    const inputData = input.data;
    const inputDims = input.dims;

    // Negative indexing
    dim = safeIndex(dim, inputDims.length);

    // Calculate the shape of the resulting array after summation
    const resultDims = inputDims.slice(); // Copy the original dimensions
    resultDims[dim] = 1; // Remove the specified axis

    // Create a new array to store the accumulated values
    // @ts-ignore
    const result = new inputData.constructor(inputData.length / inputDims[dim]);
    if (initialValue !== null) {
        result.fill(initialValue);
    }

    // Iterate over the data array
    for (let i = 0; i < inputData.length; ++i) {
        // Calculate the index in the resulting array
        let resultIndex = 0;

        for (let j = inputDims.length - 1, num = i, resultMultiplier = 1; j >= 0; --j) {
            const size = inputDims[j];
            if (j !== dim) {
                const index = num % size;
                resultIndex += index * resultMultiplier;
                resultMultiplier *= resultDims[j];
            }
            num = Math.floor(num / size);
        }

        // Accumulate the value at the current index
        result[resultIndex] = callbackfn(result[resultIndex], inputData[i], i, resultIndex);
    }

    if (!keepdim) resultDims.splice(dim, 1);

    return [input.type, result, resultDims];
}

/**
 * Calculates the standard deviation and mean over the dimensions specified by dim. dim can be a single dimension or `null` to reduce over all dimensions.
 * @param {Tensor} input the input tenso
 * @param {number|null} dim the dimension to reduce. If None, all dimensions are reduced.
 * @param {number} correction difference between the sample size and sample degrees of freedom. Defaults to Bessel's correction, correction=1.
 * @param {boolean} keepdim whether the output tensor has dim retained or not.
 * @returns {Tensor[]} A tuple of (std, mean) tensors.
 */
export function std_mean(input, dim = null, correction = 1, keepdim = false) {
    const inputData = /** @type {Float32Array} */ (input.data);
    const inputDims = input.dims;

    if (dim === null) {
        // None to reduce over all dimensions.
        const sum = inputData.reduce((a, b) => a + b, 0);
        const mean = sum / inputData.length;
        const std = Math.sqrt(inputData.reduce((a, b) => a + (b - mean) ** 2, 0) / (inputData.length - correction));

        const meanTensor = new Tensor(
            input.type,
            [mean],
            [
                /* scalar */
            ],
        );
        const stdTensor = new Tensor(
            input.type,
            [std],
            [
                /* scalar */
            ],
        );

        return [stdTensor, meanTensor];
    }
    dim = safeIndex(dim, inputDims.length);
    const meanTensor = mean(input, dim, keepdim);
    const meanTensorData = meanTensor.data;

    // Compute squared sum
    const [type, result, resultDims] = reduce_helper(
        (a, b, i, j) => a + (b - meanTensorData[j]) ** 2,
        input,
        dim,
        keepdim,
    );

    // Square root of the squared sum
    for (let i = 0; i < result.length; ++i) {
        result[i] = Math.sqrt(result[i] / (inputDims[dim] - correction));
    }

    const stdTensor = new Tensor(type, result, resultDims);

    return [stdTensor, meanTensor];
}

/**
 * Returns the mean value of each row of the input tensor in the given dimension dim.
 * @param {Tensor} input the input tensor.
 * @param {number|null} dim the dimension to reduce.
 * @param {boolean} keepdim whether the output tensor has dim retained or not.
 * @returns {Tensor} A new tensor with means taken along the specified dimension.
 */
export function mean(input, dim = null, keepdim = false) {
    const inputDims = input.dims;
    const inputData = /** @type {Float32Array} */ (input.data);

    if (dim === null) {
        // None to reduce over all dimensions.
        const val = inputData.reduce((a, b) => a + b, 0);
        return new Tensor(
            input.type,
            [val / inputData.length],
            [
                /* scalar */
            ],
        );
    }
    dim = safeIndex(dim, inputDims.length);

    // Compute sum
    const [type, result, resultDims] = reduce_helper((a, b) => a + b, input, dim, keepdim);

    // Divide by number of elements in the dimension
    if (inputDims[dim] !== 1) {
        for (let i = 0; i < result.length; ++i) {
            result[i] /= inputDims[dim];
        }
    }

    return new Tensor(type, result, resultDims);
}

function dimsToStride(dims) {
    const stride = new Array(dims.length);
    for (let i = dims.length - 1, s2 = 1; i >= 0; --i) {
        stride[i] = s2;
        s2 *= dims[i];
    }
    return stride;
}

function fullHelper(size, fill_value, dtype, cls) {
    const numElements = size.reduce((a, b) => a * b, 1);
    return new Tensor(dtype, new cls(numElements).fill(fill_value), size);
}

/**
 * Creates a tensor of size size filled with fill_value. The tensor's dtype is inferred from fill_value.
 * @param {number[]} size A sequence of integers defining the shape of the output tensor.
 * @param {number|bigint|boolean} fill_value The value to fill the output tensor with.
 * @returns {Tensor} The filled tensor.
 */
export function full(size, fill_value) {
    let dtype;
    let typedArrayCls;
    if (typeof fill_value === 'number') {
        dtype = 'float32';
        typedArrayCls = Float32Array;
    } else if (typeof fill_value === 'bigint') {
        dtype = 'int64';
        typedArrayCls = BigInt64Array;
    } else if (typeof fill_value === 'boolean') {
        dtype = 'bool';
        typedArrayCls = Uint8Array;
    } else {
        // TODO: support other dtypes
        throw new Error(`Unsupported data type: ${typeof fill_value}`);
    }
    return fullHelper(size, fill_value, dtype, typedArrayCls);
}

export function full_like(tensor, fill_value) {
    return full(tensor.dims, fill_value);
}

/**
 * Returns a tensor filled with the scalar value 1, with the shape defined by the variable argument size.
 * @param {number[]} size A sequence of integers defining the shape of the output tensor.
 * @returns {Tensor} The ones tensor.
 */
export function ones(size) {
    return fullHelper(size, 1n, 'int64', BigInt64Array);
}

/**
 * Returns a tensor filled with the scalar value 1, with the same size as input.
 * @param {Tensor} tensor The size of input will determine size of the output tensor.
 * @returns {Tensor} The ones tensor.
 */
export function ones_like(tensor) {
    return ones(tensor.dims);
}

/**
 * Returns a tensor filled with the scalar value 0, with the shape defined by the variable argument size.
 * @param {number[]} size A sequence of integers defining the shape of the output tensor.
 * @returns {Tensor} The zeros tensor.
 */
export function zeros(size) {
    return fullHelper(size, 0n, 'int64', BigInt64Array);
}

/**
 * Returns a tensor filled with the scalar value 0, with the same size as input.
 * @param {Tensor} tensor The size of input will determine size of the output tensor.
 * @returns {Tensor} The zeros tensor.
 */
export function zeros_like(tensor) {
    return zeros(tensor.dims);
}

/**
 * Returns a tensor filled with random numbers from a uniform distribution on the interval [0, 1)
 * @param {number[]} size A sequence of integers defining the shape of the output tensor.
 * @returns {Tensor} The random tensor.
 */
export function rand(size) {
    const length = size.reduce((a, b) => a * b, 1);
    return new Tensor(
        'float32',
        Float32Array.from({ length }, () => random.random()),
        size,
    );
}

/**
 * Returns a tensor filled with random numbers from a normal distribution with mean 0 and variance 1 (also called the standard normal distribution).
 * @param {number[]} size A sequence of integers defining the shape of the output tensor.
 * @returns {Tensor} The random tensor.
 */
export function randn(size) {
    const length = size.reduce((a, b) => a * b, 1);
    return new Tensor(
        'float32',
        Float32Array.from({ length }, () => random.gauss()),
        size,
    );
}

/**
 * Quantizes the embeddings tensor to binary or unsigned binary precision.
 * @param {Tensor} tensor The tensor to quantize.
 * @param {'binary'|'ubinary'} precision The precision to use for quantization.
 * @returns {Tensor} The quantized tensor.
 */
export function quantize_embeddings(tensor, precision) {
    if (tensor.dims.length !== 2) {
        throw new Error('The tensor must have 2 dimensions');
    }
    if (tensor.dims.at(-1) % 8 !== 0) {
        throw new Error('The last dimension of the tensor must be a multiple of 8');
    }
    if (!['binary', 'ubinary'].includes(precision)) {
        throw new Error("The precision must be either 'binary' or 'ubinary'");
    }

    const signed = precision === 'binary';
    const dtype = signed ? 'int8' : 'uint8';

    // Create a typed array to store the packed bits
    const cls = signed ? Int8Array : Uint8Array;
    const inputData = tensor.data;
    const outputData = new cls(inputData.length / 8);

    // Iterate over each number in the array
    for (let i = 0; i < inputData.length; ++i) {
        // Determine if the number is greater than 0
        const bit = inputData[i] > 0 ? 1 : 0;

        // Calculate the index in the typed array and the position within the byte
        const arrayIndex = Math.floor(i / 8);
        const bitPosition = i % 8;

        // Pack the bit into the typed array
        outputData[arrayIndex] |= bit << (7 - bitPosition);
        if (signed && bitPosition === 0) {
            outputData[arrayIndex] -= 128;
        }
    }

    return new Tensor(dtype, outputData, [tensor.dims[0], tensor.dims[1] / 8]);
}
