/**
 * File: ndarray_core.js
 * Responsibility: Core class definition, metadata management, memory layout algorithms, factory functions.
 */
import { NDProb } from "./ndarray_prob";
import { NDWasm, WasmRuntime } from "./ndwasm";
import { NDWasmDecomp } from "./ndwasm_decomp";
import { NDWasmAnalysis } from "./ndwasm_analysis";
import { NDWasmBlas } from "./ndwasm_blas";
import { NDWasmSignal } from "./ndwasm_signal";
import { NDWasmImage } from "./ndwasm_image";
import { NDWasmOptimize } from './ndwasm_optimize';
import { array, full, zeros } from './ndarray_factory';
import { NDWasmArray } from "./ndwasmarray";
import * as Jit from "./ndarray_jit";

/**
 * The TypedArray map
 */
export const DTYPE_MAP = {
    'float64': Float64Array,
    'float32': Float32Array,
    'int32': Int32Array,
    'uint32': Uint32Array,
    'int16': Int16Array,
    'uint16': Uint16Array,
    'int8': Int8Array,
    'uint8': Uint8Array,
    'uint8c': Uint8ClampedArray
};
/**
 * The NDArray class
 */
export class NDArray {
    /**
     * @param {TypedArray} data - The underlying physical storage.
     * @param {Object} options
     * @param {Array|Int32Array} options.shape - The dimensions of the array.
     * @param {Array|Int32Array} [options.strides] - The strides, defaults to C-style.
     * @param {number} [options.offset=0] - The view offset.
     * @param {string} [options.dtype] - The data type.
     */
    constructor(data, { shape, strides, offset = 0, dtype }) {
        this.data = data;
        this.shape = shape instanceof Int32Array ? shape : Int32Array.from(shape);
        this.ndim = this.shape.length;
        this.offset = offset;
        this.dtype = dtype || this._determineDtype(data);
        
        // 1. Calculate the total number of elements.
        this.size = 1;
        for (let i = 0; i < this.ndim; i++) this.size *= this.shape[i];

        // 2. Initialize or validate strides.
        if (strides) {
            this.strides = strides instanceof Int32Array ? strides : Int32Array.from(strides);
        } else {
            this.strides = this._computeDefaultStrides(this.shape);
        }

        // 3. Pre-calculate the memory contiguity flag (a key optimization).
        // Only contiguous memory allows for single-layer flat loops in JS and efficient copying in WASM.
        this.isContiguous = this._checkContiguity();
    }

    static random = NDProb;
    static blas = NDWasmBlas;
    static decomp = NDWasmDecomp;
    static analysis = NDWasmAnalysis;
    static image = NDWasmImage;
    static signal = NDWasmSignal;
    /**
     * Optimization module for linear programming, non-linear minimization, and linear regression.
     * @memberof NDArray
     * @type {NDWasmOptimize}
     */
    static optimize = NDWasmOptimize;

    // --- Internal private helpers ---

    _determineDtype(data) {
        for (const [name, Constructor] of Object.entries(DTYPE_MAP)) {
            if (data instanceof Constructor) return name;
        }
        return 'float64';
    }

    _computeDefaultStrides(shape) {
        const strides = new Int32Array(this.ndim);
        let s = 1;
        for (let i = this.ndim - 1; i >= 0; i--) {
            strides[i] = s;
            s *= shape[i];
        }
        return strides;
    }

    _checkContiguity() {
        if (this.ndim === 0) return true;
        let expectedStride = 1;
        for (let i = this.ndim - 1; i >= 0; i--) {
            // If a dimension's stride does not match the expected contiguous layout, it's a non-contiguous view (e.g., after transpose or slice).
            if (this.shape[i] > 1 && this.strides[i] !== expectedStride) {
                return false;
            }
            expectedStride *= this.shape[i];
        }
        return true;
    }

    /**
     * High-performance addressing: converts multidimensional indices to a physical offset.
     * @private
     * @param {Array|Int32Array} indices 
     * @param {number}
     */
    _getOffset(indices) {
        let ptr = this.offset;
        for (let i = 0; i < indices.length; i++) {
            ptr += indices[i] * this.strides[i];
        }
        return ptr;
    }
    

    /**
     * To JavaScript Array
     * @returns {Array|number} the array
     */
    toArray() {
        const { data, shape, strides, offset, ndim } = this;

        if (ndim === 0) {
            return data[offset];
        }

        /**
         * @param {number} dimIdx 
         * @param {number} currentOffset 
         */
        const recurse = (dimIdx, currentOffset) => {
            const size = shape[dimIdx];
            const stride = strides[dimIdx];
            const result = new Array(size);

            if (dimIdx === ndim - 1) {
                for (let i = 0; i < size; i++) {
                    result[i] = data[currentOffset + i * stride];
                }
            } else {
                for (let i = 0; i < size; i++) {
                    result[i] = recurse(dimIdx + 1, currentOffset + i * stride);
                }
            }
            return result;
        };

        return recurse(0, offset);
    }

    /**
     * Returns a string representation of the ndarray.
     * Formats high-dimensional data with proper indentation and line breaks.
     * @returns {String}
     */
    toString() {
        const { shape, strides, offset, ndim, data, dtype } = this;

        // Handle 0-dimensional array (scalar)
        if (ndim === 0) {
            return `array(${data[offset]}, dtype=${dtype})`;
        }

        /**
         * Recursive helper to format array dimensions.
         * @param {number} dimIdx - Current dimension index being processed.
         * @param {number} currentOffset - Current absolute memory offset in the data buffer.
         * @param {string} indent - String used for vertical alignment.
         */
        const format = (dimIdx, currentOffset, indent) => {
            // Base case: Final dimension (innermost level)
            // Returns a simple comma-separated list: [1, 2, 3]
            if (dimIdx === ndim - 1) {
                const elements = [];
                const len = shape[dimIdx];
                const stride = strides[dimIdx];
                
                for (let i = 0; i < len; i++) {
                    elements.push(data[currentOffset + i * stride]);
                }
                return '[' + elements.join(', ') + ']';
            }

            // Recursive case: High-dimensional levels
            const results = [];
            const nextDim = dimIdx + 1;
            const len = shape[dimIdx];
            const stride = strides[dimIdx];

            // Increase indentation for the next level to align with the opening bracket
            const nextIndent = indent + ' ';

            for (let i = 0; i < len; i++) {
                results.push(format(nextDim, currentOffset + i * stride, nextIndent));
            }

            // Visual formatting: 
            // 1. Matrices (depth 2) use a single newline.
            // 2. Tensors (depth 3+) use multiple newlines to create visual blocks between slices.
            const depthFromBottom = ndim - dimIdx;
            const lineBreaks = '\n'.repeat(Math.max(1, depthFromBottom - 1));
            const separator = ',' + lineBreaks + nextIndent;

            return '[' + results.join(separator) + ']';
        };

        // "array(" has a length of 6, so we start the indentation with 6 spaces
        const prefix = 'array(';
        const dataString = format(0, offset, ' '.repeat(prefix.length));
        
        return `${prefix}${dataString}, dtype=${dtype})`;
    }


     /**
     * High-performance element-wise mapping with jit compilation.
     * @param {string | Function} fnOrStr - The function string to apply to each element, like 'Math.sqrt(${val})', or a lambda expression
     * @returns {NDArray} A new array with the results.
     * 
     */
    map(fnOrStr, dtype = undefined) {
        const outDtype = dtype || this.dtype;
        const result = zeros(this.shape, outDtype);
        
        const opStr = fnOrStr.toString();
        const cacheKey = `unary|${this.dtype}|${outDtype}|${opStr}|${this.shape}|${this.strides}`;
        
        let kernel = Jit._createUnaryKernel(cacheKey, this.shape, this.strides, fnOrStr);

        kernel(this.data, result.data, this.offset, result.offset);
        return result;
    }

    


    /**
     * Generic iterator that handles stride logic. It's slow. use map if you want to use jit.
     * @param {Function} callback - A function called with `(value, index, flatPhysicalIndex)`.
     * @see NDArray#map
     */
    iterate(callback) {
        const currentIdx = new Int32Array(this.ndim);
        for (let i = 0; i < this.size; i++) {
            const ptr = this._getOffset(currentIdx);
            callback(this.data[ptr], i, ptr);
            // Increment multidimensional index
            for (let d = this.ndim - 1; d >= 0; d--) {
                if (++currentIdx[d] < this.shape[d]) break;
                currentIdx[d] = 0;
            }
        }
    }
    
    // --- Basic Arithmetic ---
    /**
     * Element-wise addition. Supports broadcasting.
     * @function
     * @param {NDArray|number} other - The array or scalar to add.
     * @returns {NDArray} A new array containing the results.
     * 
     */
    add(other){
        return this._binaryOp(other, (a, b) => a + b);
    }
    /**
     * Element-wise subtraction. Supports broadcasting.
     * @function
     * @param {NDArray|number} other - The array or scalar to subtract.
     * @returns {NDArray} A new array containing the results.
     * 
     */
    sub(other){
        return this._binaryOp(other,(a, b) => a - b);
    }
    /**
     * Element-wise multiplication. Supports broadcasting.
     * @function
     * @param {NDArray|number} other - The array or scalar to multiply by.
     * @returns {NDArray} A new array containing the results.
     * 
     */
    mul(other){
        return this._binaryOp(other,(a, b) => a * b);
    }
    /**
     * Element-wise division. Supports broadcasting.
     * @function
     * @param {NDArray|number} other - The array or scalar to divide by.
     * @returns {NDArray} A new array containing the results.
     * 
     */
    div(other){
        return this._binaryOp(other,(a, b) => a / b);
    }
    /**
     * Element-wise exponentiation. Supports broadcasting.
     * @function
     * @param {NDArray|number} other - The array or scalar exponent.
     * @returns {NDArray} A new array containing the results.
     * 
     */
    pow(other){
        return this._binaryOp(other,(a, b) => a ** b);
    }
    /**
     * Element-wise modulo. Supports broadcasting.
     * @function
     * @param {NDArray|number} other - The array or scalar divisor.
     * @returns {NDArray} A new array containing the results.
     * 
     */
    mod(other){
        return this._binaryOp(other,(a, b) => a % b);
    }

    // --- In-place Arithmetic ---
    /**
     * In-place element-wise addition.
     * @function
     * @param {NDArray|number} other - The array or scalar to add.
     * @returns {NDArray} The modified array (`this`).
     * 
     */
    iadd(other){
        return this._binaryOp(other,(a, b) => a + b, true);
    }
    /**
     * In-place element-wise subtraction.
     * @function
     * @param {NDArray|number} other - The array or scalar to subtract.
     * @returns {NDArray} The modified array (`this`).
     * 
     */
    isub(other){
        return this._binaryOp(other,(a, b) => a - b, true);
    }
    /**
     * In-place element-wise multiplication.
     * @function
     * @param {NDArray|number} other - The array or scalar to multiply by.
     * @returns {NDArray} The modified array (`this`).
     * 
     */
    imul(other){
        return this._binaryOp(other,(a, b) => a * b, true);
    }
    /**
     * In-place element-wise division.
     * @function
     * @param {NDArray|number} other - The array or scalar to divide by.
     * @returns {NDArray} The modified array (`this`).
     * 
     */
    idiv(other){
        return this._binaryOp(other,(a, b) => a / b, true);
    }
    /**
     * In-place element-wise exponentiation.
     * @function
     * @param {NDArray|number} other - The array or scalar exponent.
     * @returns {NDArray} The modified array (`this`).
     * 
     */
    ipow(other){
        return this._binaryOp(other,(a, b) => a ** b, true);
    }
    /**
     * In-place element-wise modulo.
     * @function
     * @param {NDArray|number} other - The array or scalar divisor.
     * @returns {NDArray} The modified array (`this`).
     * 
     */
    imod(other){
        return this._binaryOp(other,(a, b) => a % b, true);
    }

    /**
     * bitwise AND. Returns a new array.
     * @function
     * @param {NDArray|number} other - The array or scalar to perform the operation with.
     * @returns {NDArray}
     * 
     */
    bitwise_and(other){
        return this._binaryOp(other,(a, b) => a & b, false);
    }

    /**
     * bitwise OR. Returns a new array.
     * @function
     * @param {NDArray|number} other - The array or scalar to perform the operation with.
     * @returns {NDArray}
     * 
     */
    bitwise_or(other){
        return this._binaryOp(other,(a, b) => a | b, false);
    }

    /**
     * bitwise XOR. Returns a new array.
     * @function
     * @param {NDArray|number} other - The array or scalar to perform the operation with.
     * @returns {NDArray}
     * 
     */
    bitwise_xor(other){
        return this._binaryOp(other,(a, b) => a ^ b, false);
    }
    
    /**
     * bitwise lshift. Returns a new array.
     * @function
     * @param {NDArray|number} other - The array or scalar to perform the operation with.
     * @returns {NDArray}
     * 
     */
    bitwise_lshift(other){
        return this._binaryOp(other,(a, b) => a << b, false);
    }

    /**
     * bitwise (logical) rshift. Returns a new array.
     * @function
     * @param {NDArray|number} other - The array or scalar to perform the operation with.
     * @returns {NDArray}
     * 
     */
    bitwise_rshift(other){
        return this._binaryOp(other,(a, b) => a >>> b, false);
    }

    /**
     * bitwise NOT. Returns a new array.
     * @function
     * @returns {NDArray}
     * 
     */
    bitwise_not() { return this.map("~${val}"); };

    // --- Unary Operations ---
    /**
     * Returns a new array with the numeric negation of each element.
     * @function
     * @returns {NDArray}
     * 
     */
    neg() { return this.map("-${val}"); };

    /**
     * Returns a new array with the absolute value of each element.
     * @function
     * @returns {NDArray}
     * 
     */
    abs() { return this.map("Math.abs(${val})"); };
    /**
     * Returns a new array with `e` raised to the power of each element.
     * @function
     * @returns {NDArray}
     * 
     */
    exp() { return this.map("Math.exp(${val})"); };
    /**
     * Returns a new array with the square root of each element.
     * @function
     * @returns {NDArray}
     * 
     */
    sqrt() { return this.map("Math.sqrt(${val})"); };
     /**
     * Returns a new array with the sine of each element.
     * @function
     * @returns {NDArray}
     * 
     */
    sin() { return this.map("Math.sin(${val})"); };
    /**
     * Returns a new array with the cosine of each element.
     * @function
     * @returns {NDArray}
     * 
     */
    cos() { return this.map("Math.cos(${val})"); };
    /**
     * Returns a new array with the tangent of each element.
     * @function
     * @returns {NDArray}
     * 
     */
    tan() { return this.map("Math.tan(${val})"); };
    /**
     * Returns a new array with the natural logarithm (base e) of each element.
     * @function
     * @returns {NDArray}
     * 
     */
    log() { return this.map("Math.log(${val})"); };
    /**
     * Returns a new array with the smallest integer greater than or equal to each element.
     * @function
     * @returns {NDArray}
     * 
     */
    ceil() { return this.map("Math.ceil(${val})"); };
    /**
     * Returns a new array with the largest integer less than or equal to each element.
     * @function
     * @returns {NDArray}
     * 
     */
    floor() { return this.map("Math.floor(${val})"); };
    /**
     * Returns a new array with the value of each element rounded to the nearest integer.
     * @function
     * @returns {NDArray}
     * 
     */
    round() { return this.map("Math.round(${val})"); };

    // --- Comparison and Logic Operators ---
    /**
     * Element-wise equality comparison. Returns a new boolean (uint8) array.
     * @function
     * @param {NDArray|number} other - The array or scalar to compare with.
     * @returns {NDArray}
     * 
     */
    eq(other){
        return this._comparisonOp(other,(a, b) => a === b);
    }
    /**
     * Element-wise inequality comparison. Returns a new boolean (uint8) array.
     * @function
     * @param {NDArray|number} other - The array or scalar to compare with.
     * @returns {NDArray}
     * 
     */
    neq(other){
        return this._comparisonOp(other,(a, b) => a !== b);
    }
    /**
     * Element-wise greater-than comparison. Returns a new boolean (uint8) array.
     * @function
     * @param {NDArray|number} other - The array or scalar to compare with.
     * @returns {NDArray}
     * 
     */
    gt(other){
        return this._comparisonOp(other,(a, b) => a > b);
    }
    /**
     * Element-wise greater-than-or-equal comparison. Returns a new boolean (uint8) array.
     * @function
     * @param {NDArray|number} other - The array or scalar to compare with.
     * @returns {NDArray}
     * 
     */
    gte(other){
        return this._comparisonOp(other,(a, b) => a >= b);
    }
    /**
     * Element-wise less-than comparison. Returns a new boolean (uint8) array.
     * @function
     * @param {NDArray|number} other - The array or scalar to compare with.
     * @returns {NDArray}
     * 
     */
    lt(other){
        return this._comparisonOp(other,(a, b) => a < b);
    }
    /**
     * Element-wise less-than-or-equal comparison. Returns a new boolean (uint8) array.
     * @function
     * @param {NDArray|number} other - The array or scalar to compare with.
     * @returns {NDArray}
     * 
     */
    lte(other){
        return this._comparisonOp(other,(a, b) => a <= b);
    }

    /**
     * Element-wise logical AND. Returns a new boolean (uint8) array.
     * @function
     * @param {NDArray|number} other - The array or scalar to perform the operation with.
     * @returns {NDArray}
     * 
     */
    logical_and(other){
        return this._comparisonOp(other,(a, b) => a && b);
    }
    /**
     * Element-wise logical OR. Returns a new boolean (uint8) array.
     * @function
     * @param {NDArray|number} other - The array or scalar to perform the operation with.
     * @returns {NDArray}
     * 
     */
    logical_or(other){
        return this._comparisonOp(other,(a, b) => a || b);
    }
    /**
     * Element-wise logical NOT. Returns a new boolean (uint8) array.
     * @function
     * @returns {NDArray}
     * 
     */
    logical_not() {
        return this.map("(${val} ? 0 : 1)" );
    }

    // --- Reductions ---
    /**
     * Computes the sum of elements along the specified axis.
     * 
     * @param {number|null} [axis=null]
     * @returns {NDArray|number}
     */
    sum(axis = null) { 
        return this._reduce(axis, () => 0, (a, b) => a + b); 
    }

    /**
     * Computes the cumprod of elements along the specified axis.
     * 
     * @param {number|null} [axis=null]
     * @returns {NDArray|number}
     */
    cumprod(axis = null) { 
        return this._reduce(axis, () => 1, (a, b) => a * b); 
    }

    /**
     * Computes the arithmetic mean along the specified axis.
     * 
     * @param {number|null} [axis=null]
     * @returns {NDArray|number}
     */
    mean(axis = null) { 
        return this._reduce(axis, () => 0, (a, b) => a + b, (acc, n) => acc / n); 
    }

    /**
     * Returns the maximum value along the specified axis.
     * 
     * @param {number|null} [axis=null]
     * @returns {NDArray|number}
     */
    max(axis = null) { 
        return this._reduce(axis, () => -Infinity, (a,b) => Math.max(a,b) ); 
    }

    /**
     * Returns the minimum value along the specified axis.
     * 
     * @param {number|null} [axis=null]
     * @returns {NDArray|number}
     */
    min(axis = null) { 
        return this._reduce(axis, () => Infinity, (a,b) => Math.min(a,b) ); 
    }

    /**
     * Computes the variance along the specified axis.
     * Note: This implementation uses a two-pass approach (mean first, then squared differences).
     * Ensure that the `sub` method supports broadcasting if `axis` is not null.
     * 
     * 
     * @param {number|null} [axis=null] - The axis to reduce.
     * @returns {NDArray|number}
     */
    var(axis = null) {
        const mu = this.mean(axis);
        if (axis === null || this.ndim <= 1) {
            const diff = this.sub(mu);
            return diff.mul(diff).mean();
        }

        // Step 1: Compute the mean along the axis
        const newShape = new Int32Array(this.shape);
        newShape[axis] = 1;
        const muReshaped = mu.reshape(newShape); 

        // Step 2: Compute (x - mu)^2. 
        const diff = this.sub(muReshaped);

        return diff.mul(diff).mean(axis);
    }
    
    /**
     * Computes the standard deviation along the specified axis.
     * 
     * 
     * @param {number|null} [axis=null] - The axis to reduce.
     * @returns {NDArray|number}
     */
    std(axis = null) {
        const v = this.var(axis);
        // If the result is an NDArray (axis reduction), return the element-wise square root.
        // Otherwise, return the square root of the scalar number.
        return v instanceof NDArray ? v.sqrt() : Math.sqrt(v);
    }

   /**
     * Internal generic reduction engine with optimizations for different memory layouts.
     * 
     * @param {number|null} axis - The axis to reduce. If null, a global reduction is performed.
     * @param {Function} initFn - Returns the initial value for the accumulator (e.g., () => 0).
     * @param {Function} reducer - The binary reduction function (e.g., (acc, val) => acc + val).
     * @param {Function} [finalFn] - Applied to the final result (e.g., (sum, count) => sum / count).
     * @returns {NDArray|number} A scalar number for global reduction, or an NDArray for axis reduction.
     * @private
     */
     _reduce(axis, initFn, reducer, finalFn = undefined) {
        const ndim = this.ndim;
        if (ndim === 0) {
            const val = this.data[this.offset];
            return finalFn ? finalFn(val, 1) : val;
        }

        // 1. Determine which axes to iterate over and which to reduce
        let reduceAxes = [];
        if (axis === null || axis === undefined) {
            // Global reduction: reduce all axes
            reduceAxes = Array.from({ length: ndim }, (_, i) => i);
        } else {
            // Single axis reduction
            let a = axis < 0 ? axis + ndim : axis;
            reduceAxes = [a];
        }

        const iterAxes = Array.from({ length: ndim }, (_, i) => i).filter(ax => !reduceAxes.includes(ax));
        
        // 2. Cache management
        const opStr = reducer.toString() + (finalFn ? finalFn.toString() : '');
        const cacheKey = `red|${this.dtype}|${opStr}|${this.shape}|${this.strides}|${reduceAxes}`;
        
        let kernel = Jit._createReduceKernel(cacheKey, this.shape, this.strides, iterAxes, reduceAxes, reducer, finalFn);

        // 3. Prepare buffers and initial context
        let reduceCount = reduceAxes.reduce((a, b) => a * this.shape[b], 1);
        if(reduceCount == 0) { reduceCount = 1; }
        let outSize = this.size / reduceCount;
        if(outSize == 0) { outSize = 1; }
        const outData = new DTYPE_MAP[this.dtype](outSize);
        const initVal = initFn();

        // 4. Run the zero-dependency JIT kernel
        kernel(this.data, outData, this.offset, initVal, reduceCount);

        // Return scalar for global reduction, otherwise return NDArray
        if (axis === null || axis === undefined) return outData[0];
        
        const newShape = this.shape.filter((_, i) => !reduceAxes.includes(i));
        return new NDArray(outData, { shape: newShape, dtype: this.dtype });
    }


    // --- Index Operators ---
    /**
     * Returns the index of the maximum value in a flattened array.
     * @returns {number}
     * 
     */
    argmax() {
        let maxV = -Infinity, maxIdx = -1;
        this.iterate((v, i) => { if (v > maxV) { maxV = v; maxIdx = i; } });
        return maxIdx;
    }
    /**
     * Returns the index of the minimum value in a flattened array.
     * @returns {number}
     * 
     */
    argmin() {
        let minV = Infinity, minIdx = -1;
        this.iterate((v, i) => { if (v < minV) { minV = v; minIdx = i; } });
        return minIdx;
    }

    // --- Logical tools ---
    /**
     * Checks if all elements in the array are truthy.
     * @returns {boolean}
     * 
     */
    all() {
        let result = true;
        this.iterate(v => { if (!v) { result = false; } }); // Cannot break early
        return result;
    }
    /**
     * Checks if any element in the array is truthy.
     * @returns {boolean}
     * 
     */
    any() {
        let result = false;
        this.iterate(v => { if (v) { result = true; } }); // Cannot break early
        return result;
    }











    //------------view functions------------- 
    /**
     * Returns a new array with a new shape, without changing data. O(1) operation.
     * This only works for contiguous arrays. If the array is not contiguous,
     * you must call .copy() first.
     * 
     * @param {...number} newShape - The new shape.
     * @returns {NDArray} NDArray view.
     */
    reshape(...newShape) {
        if (Array.isArray(newShape[0]) || ArrayBuffer.isView(newShape[0])) newShape = newShape[0];

        const newSize = newShape.reduce((a, b) => a * b, 1);
        if (newSize !== this.size) {
            throw new Error(`Cannot reshape array of size ${this.size} into shape [${newShape}] = ${newSize}`);
        }

        if (this.isContiguous) {
            // For a contiguous array, reshaping is just a matter of creating a new
            // view. The NDArray constructor will compute the new strides automatically.
            return new NDArray(this.data, {
                shape: newShape,
                offset: this.offset,
                dtype: this.dtype
            });
        }

        // Reshaping a non-contiguous view is ambiguous as it would break spatial locality.
        throw new Error("Reshape of non-contiguous view is ambiguous. Use .copy().reshape() first.");
    }

    /**
     * Returns a new view of the array with axes transposed. O(1) operation.
     * 
     * @param {...number} axes - The new order of axes, e.g., [1, 0] for a matrix transpose. If not specified, reverses the order of the axes.
     * @returns {NDArray} NDArray view.
     */
    transpose(...axes) {
        if (axes.length === 0) {
            // Default: reverse all dimensions
            axes = Array.from({ length: this.ndim }, (_, i) => this.ndim - 1 - i);
        } else if (Array.isArray(axes[0])) {
            axes = axes[0];
        }

        const newShape = new Int32Array(this.ndim);
        const newStrides = new Int32Array(this.ndim);

        for (let i = 0; i < this.ndim; i++) {
            const axis = axes[i];
            newShape[i] = this.shape[axis];
            newStrides[i] = this.strides[axis];
        }

        return new NDArray(this.data, {
            shape: newShape,
            strides: newStrides,
            offset: this.offset,
            dtype: this.dtype
        });
    }

   /**
     * Returns a new view of the array sliced along each dimension.
     * This implementation strictly follows NumPy's basic slicing logic.
     * 
     * 
     * @param {...(Array|number|null|undefined)} specs - Slice parameters for each dimension.
     * - number: Scalar indexing. Picks a single element and reduces dimensionality (e.g., arr[0]).
     * - [start, end, step]: Range slicing (e.g., arr[start:end:step]).
     * - []/null/undefined: Selects the entire dimension (e.g., arr[:]).
     * 
     * @returns {NDArray} A new O(1) view of the underlying data.
     * @throws {Error} If a scalar index is out of bounds or step is zero.
     */
    slice(...specs) {
        let newOffset = this.offset;
        const newShape = [];
        const newStrides = [];

        for (let i = 0; i < this.ndim; i++) {
            const spec = i < specs.length ? specs[i] : null;
            const size_i = this.shape[i];
            const stride_i = this.strides[i];

            if (typeof spec === 'number') {
                // --- Scalar Indexing (Dimension Reduction) ---
                let idx = spec < 0 ? size_i + spec : spec;
                
                // NumPy throws error for out-of-bounds scalar indexing
                if (idx < 0 || idx >= size_i) {
                    throw new Error(`Index ${spec} is out of bounds for axis ${i} with size ${size_i}`);
                }
                newOffset += idx * stride_i;
            } else {
                // --- Range Slicing (Dimension Preserved) ---
                let start, end, step;

                if (Array.isArray(spec)) {
                    [start = null, end = null, step = 1] = spec;
                } else {
                    // Handles null, undefined, or missing specs
                    start = end = null;
                    step = 1;
                }

                if (step === null || step === undefined) step = 1;
                if (step === 0) throw new Error("Slice step cannot be zero");

                // 1. Convert negative indices
                if (start !== null) start = start < 0 ? size_i + start : start;
                if (end !== null) end = end < 0 ? size_i + end : end;

                // 2. Determine defaults and clamp boundaries based on step direction
                if (step > 0) {
                    // Positive step: default range [0, size], clamped to [0, size]
                    start = start === null ? 0 : Math.max(0, Math.min(start, size_i));
                    end = end === null ? size_i : Math.max(0, Math.min(end, size_i));
                } else {
                    // Negative step: default range [size-1, -1], clamped to [-1, size-1]
                    start = start === null ? size_i - 1 : Math.max(-1, Math.min(start, size_i - 1));
                    end = end === null ? -1 : Math.max(-1, Math.min(end, size_i - 1));
                }

                // 3. Calculate the number of elements in this slice
                // Formula: max(0, ceil((end - start) / step))
                const sliceLen = Math.max(0, Math.ceil((end - start) / step));

                // 4. Update offset for the first element of this slice
                newOffset += start * stride_i;

                newShape.push(sliceLen);
                newStrides.push(stride_i * step);
            }
        }

        return new NDArray(this.data, {
            shape: newShape,
            strides: newStrides,
            offset: newOffset,
            dtype: this.dtype
        });
    }

    /**
     * Returns a 1D view of the i-th row.
     * Only applicable to 2D arrays.
     * 
     * @param {number} i - The row index.
     * @returns {NDArray} A 1D NDArray view.
     */
    rowview(i) {
        if (this.ndim !== 2) {
            throw new Error(`rowview requires 2D array, but got ${this.ndim}D`);
        }
        return this.slice(i, null);
    }

    /**
     * Returns a 1D view of the j-th column.
     * Only applicable to 2D arrays.
     * 
     * @param {number} j - The column index.
     * @returns {NDArray} A 1D NDArray view.
     */
    colview(j) {
        if (this.ndim !== 2) {
            throw new Error(`colview requires 2D array, but got ${this.ndim}D`);
        }
        return this.slice(null, j);
    }

    /**
     * Remove axes of length one from the shape. O(1) operation.
     * 
     * @param {number|null} axis - The axis to squeeze. If null, all axes of length 1 are removed.
     * @returns {NDArray} NDArray view.
     */
    squeeze(axis = null) {
        const newShape = [];
        const newStrides = [];
        for (let i = 0; i < this.ndim; i++) {
            const isTargetAxis = (axis !== null && i === axis) || (axis === null && this.shape[i] === 1);
            if (this.shape[i] === 1 && isTargetAxis) {
                // This axis is squeezed out.
            } else {
                newShape.push(this.shape[i]);
                newStrides.push(this.strides[i]);
            }
        }

        return new NDArray(this.data, {
            shape: newShape,
            strides: newStrides,
            offset: this.offset,
            dtype: this.dtype
        });
    }

    /**
     * Returns a new, contiguous array with the same data. O(n) operation.
     * This converts any view (transposed, sliced) into a new array with a standard C-style memory layout.
     * 
     * @param {string | undefined} the target dtype
     * @returns {NDArray} NDArray view.
     */
    copy(dtype = undefined) {
        // We use the string '${val}' as the operation for the kernel
        return this.map('(${val})', dtype || this.dtype);
    }


    /**
     * Ensures the returned array has a contiguous memory layout.
     * If the array is already contiguous, it returns itself. Otherwise, it returns a copy.
     * Often used as a pre-processing step before calling WASM or other libraries.
     * 
     * @returns {NDArray} NDArray view.
     */
    asContiguous() {
        return this.isContiguous ? this : this.copy();
    }

    /**
     * Gets a single element from the array.
     * Note: This has higher overhead than batch operations. Use with care in performance-critical code.
     * 
     * @param {...number} indices - The indices of the element to get.
     * @returns {number}
     */
    get(...indices) {
        if (Array.isArray(indices[0])) indices = indices[0];
        return this.data[this._getOffset(indices)];
    }


     /**
     * Sets value(s) in the array using a unified, JIT-optimized engine.
     * Supports scalar indexing, fancy (array) indexing, and NumPy-style broadcasting.
     * 
     * @param {number|Array|NDArray} value - The source data to assign.
     * @param {...(number|Array|null)} indices - Indices for each dimension.
     * @returns {NDArray}
     */
    set(value, ...indices) {
        if(this.size === 0) {//no op for empty array
            return this;
        }
        const indexPickSets = []; 
        const targetShape = [];   
        const isDimReduced = new Array(this.ndim); 
        const hasPSet = new Uint8Array(this.ndim);

        // 1. Normalize indices into pick-sets and compute target logical shape
        for (let i = 0; i < this.ndim; i++) {
            let spec = i < indices.length ? indices[i] : null;

            if (spec === null || spec === undefined) {
                indexPickSets.push(null);
                targetShape.push(this.shape[i]);
                isDimReduced[i] = false;
                hasPSet[i] = 0;
            } else if (typeof spec === 'number') {
                let idx = spec < 0 ? this.shape[i] + spec : spec;
                if (idx < 0 || idx >= this.shape[i]) {
                    throw new Error(`Index ${idx} out of bounds for axis ${i}`);
                }
                indexPickSets.push(new Int32Array([idx]));
                isDimReduced[i] = true;
                hasPSet[i] = 1;
            } else {
                const pSet = spec instanceof Int32Array ? spec : Int32Array.from(spec);
                for (let k = 0; k < pSet.length; k++) {
                    if (pSet[k] < 0) pSet[k] += this.shape[i];
                    if (pSet[k] < 0 || pSet[k] >= this.shape[i]) {
                        throw new Error(`Index ${pSet[k]} out of bounds for axis ${i}`);
                    }
                }
                indexPickSets.push(pSet);
                targetShape.push(pSet.length);
                isDimReduced[i] = false;
                hasPSet[i] = 1;
            }
        }

        // 2. Standardize source NDArray
        const src = value instanceof NDArray ? value : array(value, this.dtype);
        
        // 3. Align source strides with target's physical axes (NumPy-style Broadcasting)
        const alignedSrcStrides = new Int32Array(this.ndim);
        let targetDimIdx = targetShape.length - 1;
        let srcDimIdx = src.ndim - 1;

        for (let i = this.ndim - 1; i >= 0; i--) {
            if (isDimReduced[i]) {
                alignedSrcStrides[i] = 0;
            } else {
                if (srcDimIdx >= 0) {
                    const sDim = src.shape[srcDimIdx];
                    const tDim = targetShape[targetDimIdx];
                    if (sDim !== tDim && sDim !== 1) {
                        throw new Error(`Incompatible broadcast: src [${src.shape}] -> [${targetShape}]`);
                    }
                    alignedSrcStrides[i] = (sDim === 1) ? 0 : src.strides[srcDimIdx];
                    srcDimIdx--;
                } else {
                    alignedSrcStrides[i] = 0;
                }
                targetDimIdx--;
            }
        }

        // 4. Handle empty selections
        const totalSize = targetShape.reduce((a, b) => a * b, 1);
        if (totalSize === 0) return this;

        // 5. JIT Dispatch
        const cacheKey = `set|${this.ndim}|${hasPSet}|${isDimReduced}|${this.strides}|${alignedSrcStrides}|${targetShape}`;
        const kernel = Jit._createSetKernel(
            cacheKey, 
            this.ndim, 
            targetShape, 
            this.strides, 
            alignedSrcStrides, 
            hasPSet, 
            isDimReduced
        );

        kernel(this.data, src.data, this.offset, src.offset, indexPickSets);

        return this;
    };


    /**
     * Advanced Indexing (Fancy Indexing).
     * Returns a physical COPY of the selected data using a JIT-compiled engine.
     * Picks elements along each dimension.
     * Note: unlike numpy, for advanced (fancy) indexing, output shape won't be reordered. 
     * Dim for 1-element advanced indexing won't be removed, either.
     * 
     * @param {...(number[]|TypedArray|number|null|undefined)} specs - Index selectors. null/undefined means select all
     */
    pick(...specs) {
        const indexPickSets = [];
        const resultShape = [];
        const isDimReduced = new Array(this.ndim);
        const isFullSlice = new Uint8Array(this.ndim); 
        const odometerShape = new Int32Array(this.ndim);
        
        // 1. Normalize specs and calculate output metadata
        for (let i = 0; i < this.ndim; i++) {
            let spec = i < specs.length ? specs[i] : null;

            if (spec === null || spec === undefined) {
                // Wildcard ":"
                const indices = new Int32Array(this.shape[i]);
                for (let k = 0; k < this.shape[i]; k++) indices[k] = k;
                indexPickSets.push(indices);
                resultShape.push(this.shape[i]);
                isFullSlice[i] = 1;
                isDimReduced[i] = false;
            } else if (typeof spec === 'number') {
                // Scalar index: reduces dimensionality
                let idx = spec < 0 ? this.shape[i] + spec : spec;
                if (idx < 0 || idx >= this.shape[i]) {
                    throw new Error(`Index ${spec} out of bounds for axis ${i}`);
                }
                indexPickSets.push(new Int32Array([idx]));
                isFullSlice[i] = 0;
                isDimReduced[i] = true;
            } else {
                // Fancy Indexing: preserves dimension
                const indices = spec instanceof Int32Array ? spec : Int32Array.from(spec);
                for (let k = 0; k < indices.length; k++) {
                    if (indices[k] < 0) indices[k] += this.shape[i];
                    if (indices[k] < 0 || indices[k] >= this.shape[i]) {
                        throw new Error(`Index ${indices[k]} out of bounds for axis ${i}`);
                    }
                }
                indexPickSets.push(indices);
                resultShape.push(indices.length);
                isFullSlice[i] = 0;
                isDimReduced[i] = false;
            }
            odometerShape[i] = indexPickSets[i].length;
        }

        // 2. Prepare output buffer
        const resultSize = resultShape.reduce((a, b) => a * b, 1);
        const Constructor = DTYPE_MAP[this.dtype];
        const newData = new Constructor(resultSize);
        if (resultSize === 0) {
            return new NDArray(newData, { shape: resultShape, dtype: this.dtype });
        }

        // 3. JIT Dispatch
        // Cache key includes structure, strides, and the specific selection pattern
        const cacheKey = `pick|${this.ndim}|${isFullSlice}|${isDimReduced}|${this.strides}|${odometerShape}`;
        
        const kernel = Jit._createPickKernel(
            cacheKey,
            this.ndim,
            this.strides,
            isFullSlice,
            isDimReduced,
            odometerShape
        );

        // 4. Execute kernel
        kernel(this.data, newData, this.offset, indexPickSets);

        return new NDArray(newData, {
            shape: resultShape,
            dtype: this.dtype
        });
    }



    /**
     * Responsibility: Implements element-wise filtering.
     * Returns a NEW 1D contiguous NDArray (Copy).
     * Filters elements based on a predicate function or a boolean mask.
     * 
     * 
     * @param {Function|Array|NDArray} predicateOrMask - A function returning boolean, 
     *        or an array/NDArray of the same shape/size containing truthy/falsy values.
     * @returns {NDArray} A new 1D NDArray containing the matched elements.
     */
    filter(predicateOrMask) {
        const results = [];
        const shape = this.shape;
        const strides = this.strides;
        const ndim = this.ndim;

        // --- Preparation: Determine if we are using a callback or a mask ---
        const isCallback = typeof predicateOrMask === 'function';
        let maskData = null;
        let maskOffset = 0;
        let maskStrides = null;

        if (!isCallback) {
            // If it's an Array/NDArray, ensure we can iterate it alongside 'this'
            if (predicateOrMask instanceof NDArray) {
                if (predicateOrMask.size !== this.size) {
                    throw new Error("Mask size must match array size");
                }
                // If it's an NDArray, we'll use its internal data and strides
                maskData = predicateOrMask.data;
                maskOffset = predicateOrMask.offset;
                maskStrides = predicateOrMask.strides;
                // Note: We assume the mask has a broadcastable or identical shape.
                // For simplicity, we assume same shape or flattened match.
            } else if (Array.isArray(predicateOrMask)) {
                if (predicateOrMask.length !== this.size) {
                    throw new Error("Mask length must match array size");
                }
                maskData = predicateOrMask;
            }
        }

        // --- Optimized Traversal ---
        const currentIdx = new Int32Array(ndim);
        let currPos = this.offset;
        let currMaskPos = maskOffset;

        for (let n = 0; n < this.size; n++) {
            const val = this.data[currPos];
            let keep = false;

            if (isCallback) {
                // Callback receives: value, current multi-dim index, and the array itself
                keep = predicateOrMask(val, currentIdx, this);
            } else {
                // Mask lookup
                keep = maskStrides 
                    ? maskData[currMaskPos] 
                    : maskData[n]; // Plain array is assumed flattened
            }

            if (keep) {
                results.push(val);
            }

            // Incremental pointer update for the source array
            for (let d = ndim - 1; d >= 0; d--) {
                if (++currentIdx[d] < shape[d]) {
                    currPos += strides[d];
                    if (maskStrides) currMaskPos += maskStrides[d];
                    break;
                } else {
                    currentIdx[d] = 0;
                    currPos -= (shape[d] - 1) * strides[d];
                    if (maskStrides) currMaskPos -= (shape[d] - 1) * maskStrides[d];
                }
            }
        }

        // Return as a 1D NDArray
        const Constructor = DTYPE_MAP[this.dtype];
        return new NDArray(new Constructor(results), {
            shape: [results.length],
            dtype: this.dtype
        });
    }

    /**
     * @return {NDArray} - new flatten view to the array
     */
    flatten(){
        return this.slice().reshape(this.size);
    }

    




    //--------------------NDWasm---------------------
    /**
     * Projects the current ndarray to a WASM proxy (WasmBuffer).
     * 
     * @param {WasmRuntime} runtime 
     * @returns {WasmBuffer} A WasmBuffer instance representing the NDArray in WASM memory.
     */
    toWasm(runtime) {
        const bridge = runtime.createBuffer(this.size, this.dtype);
        
        if (this.isContiguous) {
            // Fast path for contiguous data
            bridge.push(this.data.subarray(this.offset, this.offset + this.size));
        } else {
            // High-speed flattening path for non-contiguous views
            const target = bridge.view;
            
            // Re-use or create a "Flatten" kernel (Identity Unary Kernel)
            const cacheKey = `toWasm|${this.dtype}|${this.shape}|${this.strides}`;
            // '${val}' tells the kernel to copy the value as-is
            let kernel = Jit._createUnaryKernel(cacheKey, this.shape, this.strides, '${val}');

            // Execute the JIT kernel: copies from this.data (non-contiguous) 
            // to target (contiguous) starting at target index 0.
            kernel(this.data, target, this.offset, 0);            
        }
        return bridge;
    }

    /**
     * push to wasm
     * @returns {NDWasmArray}
     */
    push() {
        if (!NDWasm.runtime?.isLoaded) throw new Error("WasmRuntime not bound.");
        const buffer = this.toWasm(NDWasm.runtime);
        return new NDWasmArray(buffer, this.shape, this.dtype);
    }


    /**
     * Calculates the trace of a 2D square matrix (sum of diagonal elements).
     * Complexity: O(n)
     * 
     * @returns {number} The sum of the diagonal elements.
     * @throws {Error} If the array is not 2D or not a square matrix.
     */
    trace() {
        return NDWasmBlas.trace(this);
    }

    /**
     * Performs matrix multiplication. This is a wrapper around `NDWasmBlas.matmul`.
     * @param {NDArray} other The right-hand side matrix.
     * @returns {NDArray} The result of the matrix multiplication.
     * @see NDWasmBlas.matmul
     * 
     */
    matmul(other) { return NDWasmBlas.matmul(this, other); }
    /**
     * Computes the matrix power. This is a wrapper around `NDWasmBlas.matPow`.
     * @param {number} k The exponent.
     * @returns {NDArray} The result of the matrix power.
     * @see NDWasmBlas.matPow
     * 
     */
    matPow(k) { return NDWasmBlas.matPow(this, k); }
    /**
     * Performs batched matrix multiplication. This is a wrapper around `NDWasmBlas.matmulBatch`.
     * @param {NDArray} other The right-hand side batch of matrices.
     * @returns {NDArray} The result of the batched matrix multiplication.
     * @see NDWasmBlas.matmulBatch
     * 
     */
    matmulBatch(other) { return NDWasmBlas.matmulBatch(this, other); }
    /**
     * Performs matrix-vector multiplication. This is a wrapper around `NDWasmBlas.matVecMul`.
     * @param {NDArray} vec The vector to multiply by.
     * @returns {NDArray} The resulting vector.
     * @see NDWasmBlas.matVecMul
     * 
     */
    matVecMul(vec) { return NDWasmBlas.matVecMul(this, vec); }
    /**
     * Performs a symmetric rank-k update. This is a wrapper around `NDWasmBlas.syrk`.
     * @returns {NDArray} The resulting symmetric matrix.
     * @see NDWasmBlas.syrk
     * 
     */
    syrk() { return NDWasmBlas.syrk(this); }
    /**
     * Computes the vector outer product. This is a wrapper around `NDWasmBlas.ger`.
     * @param {NDArray} other The other vector.
     * @returns {NDArray} The resulting matrix.
     * @see NDWasmBlas.ger
     * 
     */
    ger(other) { return NDWasmBlas.ger(this, other); }
    /**
     * Computes the Kronecker product. This is a wrapper around `NDWasmAnalysis.kronecker`.
     * @param {NDArray} other The other matrix.
     * @returns {NDArray} The result of the Kronecker product.
     * @see NDWasmAnalysis.kronecker
     * 
     */
    kronecker(other) { return NDWasmAnalysis.kronecker(this, other); }

    // 2. Decompositions & Solvers
    /**
     * Solves a system of linear equations. This is a wrapper around `NDWasmDecomp.solve`.
     * @param {NDArray} b The right-hand side matrix or vector.
     * @returns {NDArray} The solution matrix.
     * @see NDWasmDecomp.solve
     * 
     */
    solve(b) { return NDWasmDecomp.solve(this, b); }
    /**
     * Computes the multiplicative inverse of the matrix. This is a wrapper around `NDWasmDecomp.inv`.
     * @returns {NDArray} The inverted matrix.
     * @see NDWasmDecomp.inv
     * 
     */
    inv() { return NDWasmDecomp.inv(this); }
    /**
     * Computes the Moore-Penrose pseudo-inverse of the matrix. This is a wrapper around `NDWasmDecomp.pinv`.
     * @returns {NDArray} The pseudo-inverted matrix.
     * @see NDWasmDecomp.pinv
     * 
     */
    pinv() { return NDWasmDecomp.pinv(this); }
    /**
     * Computes the Singular Value Decomposition (SVD). This is a wrapper around `NDWasmDecomp.svd`.
     * @returns {{{q: NDArray, r: NDArray}}} An object containing the U, S, and V matrices.
     * @see NDWasmDecomp.svd
     * 
     */
    svd() { return NDWasmDecomp.svd(this); }
    /**
     * Computes the QR decomposition. This is a wrapper around `NDWasmDecomp.qr`.
     * @returns {Object} An object containing the Q and R matrices.
     * @see NDWasmDecomp.qr
     * 
     */
    qr() { return NDWasmDecomp.qr(this); }
    /**
     * Computes the Cholesky decomposition. This is a wrapper around `NDWasmDecomp.cholesky`.
     * @returns {NDArray} The lower triangular matrix L.
     * @see NDWasmDecomp.cholesky
     * 
     */
    cholesky() { return NDWasmDecomp.cholesky(this); }
    /**
     * Computes the determinant of the matrix. This is a wrapper around `NDWasmDecomp.det`.
     * @returns {number} The determinant.
     * @see NDWasmDecomp.det
     * @memberof NDWasmDecomp.prototype
     */
    det() { return NDWasmDecomp.det(this); }
    /**
     * Computes the log-determinant of the matrix. This is a wrapper around `NDWasmDecomp.logDet`.
     * @returns {{{sign: number, logAbsDet: number}}} An object containing the sign and log-absolute-determinant.
     * @see NDWasmDecomp.logDet
     * @memberof NDWasmDecomp.prototype
     */
    logDet() { return NDWasmDecomp.logDet(this); }
    /**
     * Computes the LU decomposition. This is a wrapper around `NDWasmDecomp.lu`.
     * @returns {NDArray} The LU matrix.
     * @see NDWasmDecomp.lu
     * 
     */
    lu() { return NDWasmDecomp.lu(this); }

    /**
     * Computes the eigenvalues and eigenvectors of a general square matrix.
     * @returns {{values: NDArray, vectors: NDArray}} An object containing eigenvalues and eigenvectors.
     * @see NDWasmDecomp.eigen
     */
    eigen() { return NDWasmDecomp.eigen(this); }

    // 3. Signal Processing
    /**
     * Computes the 1D Fast Fourier Transform. This is a wrapper around `NDWasmSignal.fft`.
     * @this {{NDArray}} Complex array with shape [..., 2].
     * @returns {{NDArray}} Complex result with shape [..., 2].
     * @see NDWasmSignal.fft
     * 
     */
    fft() { return NDWasmSignal.fft(this); }
    /**
     * Computes the 1D Inverse Fast Fourier Transform. This is a wrapper around `NDWasmSignal.ifft`.
     * @this {{NDArray}} Complex array with shape [..., 2].
     * @returns {{NDArray}} Complex result with shape [..., 2].
     * @see NDWasmSignal.ifft
     * 
     */
    ifft() { return NDWasmSignal.ifft(this, imag); }
    /**
     * Computes the 1D Real-to-Complex Fast Fourier Transform. This is a wrapper around `NDWasmSignal.rfft`.
     * @this {{NDArray}} real input array.
     * @returns {{NDArray}} Complex result with shape [..., 2].
     * @see NDWasmSignal.rfft
     * 
     */
    rfft() { return NDWasmSignal.rfft(this); }
    /**
     * 1D Complex-to-Real Inverse Fast Fourier Transform.
     * The input must be a complex array of shape [k, 2], where k is n/2 + 1. This is a wrapper around `NDWasmSignal.rifft`.
    *  @returns {NDArray} Real-valued time domain signal.
     * @this {NDArray} Complex frequency signal of shape [n/2 + 1, 2]
     * @param {number} n - Length of the original real signal.
     * @see NDWasmSignal.rifft
     * 
     */
    rifft(n) { return NDWasmSignal.rifft(this, n); }
    /**
     * Computes the 2D Fast Fourier Transform. The input array must be 3D with shape [rows, cols, 2].
     * This is a wrapper around `NDWasmSignal.fft2`.
     * @returns {NDArray} 2D Complex result, with the same shape as input.
     * @see NDWasmSignal.fft2
     * 
     */
    fft2() { return NDWasmSignal.fft2(this); }
    /**
     * 2D Inverse Complex-to-Complex Fast Fourier Transform.
     * The input array must be 3D with shape [rows, cols, 2].
     * The transform is performed in-place.
     * This is a wrapper around `NDWasmSignal.ifft2`.
     * @returns {NDArray} 2D Complex result, with the same shape as input.
     * @see NDWasmSignal.ifft2
     * 
     */
    ifft2() { return NDWasmSignal.ifft2(this); }
    /**
     * Computes the 1D Discrete Cosine Transform. This is a wrapper around `NDWasmSignal.dct`.
     * @returns {NDArray} The result of the DCT.
     * @see NDWasmSignal.dct
     * 
     */
    dct() { return NDWasmSignal.dct(this); }
    /**
     * Performs 2D spatial convolution. This is a wrapper around `NDWasmSignal.conv2d`.
     * @param {NDArray} kernel The convolution kernel.
     * @param {number} stride The stride.
     * @param {number} padding The padding.
     * @returns {NDArray} The convolved array.
     * @see NDWasmSignal.conv2d
     * 
     */
    conv2d(kernel, stride, padding) { return NDWasmSignal.conv2d(this, kernel, stride, padding); }
    /**
     * Performs 2D spatial cross-correlation. This is a wrapper around `NDWasmSignal.correlate2d`.
     * @param {NDArray} kernel The correlation kernel.
     * @param {number} stride The stride.
     * @param {number} padding The padding.
     * @returns {NDArray} The correlated array.
     * @see NDWasmSignal.correlate2d
     * 
     */
    correlate2d(kernel, stride, padding) { return NDWasmSignal.correlate2d(this, kernel, stride, padding); }

    // 4. Analysis & Stats
    /**
     * Returns the indices that would sort the array. This is a wrapper around `NDWasmAnalysis.argsort`.
     * @returns {NDArray} An array of indices.
     * @see NDWasmAnalysis.argsort
     * 
     */
    argsort() { return NDWasmAnalysis.argsort(this); }
    /**
     * Finds the top K largest or smallest elements. This is a wrapper around `NDWasmAnalysis.topk`.
     * @param {number} k The number of elements to find.
     * @param {boolean} largest Whether to find the largest or smallest elements.
     * @returns {{values: NDArray, indices: NDArray}} An object containing the values and indices of the top K elements.
     * @see NDWasmAnalysis.topk
     * 
     */
    topk(k, largest) { return NDWasmAnalysis.topk(this, k, largest); }
    /**
     * Computes the covariance matrix. This is a wrapper around `NDWasmAnalysis.cov`.
     * @returns {NDArray} The covariance matrix.
     * @see NDWasmAnalysis.cov
     * 
     */
    cov() { return NDWasmAnalysis.cov(this); }
    /**
     * Computes the matrix norm. This is a wrapper around `NDWasmAnalysis.norm`.
     * @param {number} type The type of norm to compute.
     * @returns {number} The norm of the matrix.
     * @see NDWasmAnalysis.norm
     * 
     */
    norm(type) { return NDWasmAnalysis.norm(this, type); }
    /**
     * Computes the rank of the matrix. This is a wrapper around `NDWasmAnalysis.rank`.
     * @param {number} tol The tolerance for singular values.
     * @returns {number} The rank of the matrix.
     * @see NDWasmAnalysis.rank
     * 
     */
    rank(tol) { return NDWasmAnalysis.rank(this, tol); }
    /**
     * Computes the eigenvalue decomposition for a symmetric matrix. This is a wrapper around `NDWasmAnalysis.eigenSym`.
     * @param {boolean} vectors Whether to compute the eigenvectors.
     * @returns {{values: NDArray, vectors: NDArray|null}} An object containing the eigenvalues and eigenvectors.
     * @see NDWasmAnalysis.eigenSym
     * 
     */
    eigenSym(vectors) { return NDWasmAnalysis.eigenSym(this, vectors); }
    /**
     * Estimates the reciprocal condition number of the matrix. This is a wrapper around `NDWasmAnalysis.cond`.
     * @param {number} norm The norm type.
     * @returns {number} The reciprocal condition number.
     * @see NDWasmAnalysis.cond
     * 
     */
    cond(norm=1) { return NDWasmAnalysis.cond(this, norm); }
    
    // 5. Spatial
    /**
     * Computes the pairwise distances between two sets of vectors. This is a wrapper around `NDWasmAnalysis.pairwiseDist`.
     * @param {NDArray} other The other set of vectors.
     * @returns {NDArray} The distance matrix.
     * @see NDWasmAnalysis.pairwiseDist
     * 
     */
    pairwiseDist(other) { return NDWasmAnalysis.pairwiseDist(this, other); }
    /**
     * Performs K-Means clustering. This is a wrapper around `NDWasmAnalysis.kmeans`.
     * @param {number} k The number of clusters.
     * @param {number} maxIter The maximum number of iterations.
     * @returns {{centroids: NDArray,labels: NDArray,iterations: number}} An object containing the centroids, labels, and number of iterations.
     * @see NDWasmAnalysis.kmeans
     * 
     */
    kmeans(k, maxIter) { return NDWasmAnalysis.kmeans(this, k, maxIter); }






    _binaryOp(other, opFn, isInplace = false) {
        // Unify scalars by wrapping them into a 1D/0D NDArray.
        // broadcastShapes will naturally assign a stride of 0 to broadcasted dimensions.
        const b = (typeof other === 'number') ? array([other]) : other;
        const { outShape, strideA, strideB } = broadcastShapes(this, b);
        const result = isInplace ? this : zeros(outShape, this.dtype);
        
        const strideOut = result.strides;
        const opStr = opFn.toString();

        // Unique key for the specific memory layout and operation
        const cacheKey = `bin|${this.dtype}|${result.dtype}|${opStr}|${outShape}|${strideA}|${strideB}|${strideOut}`;
        
        let kernel = Jit._createBinKernel(cacheKey, outShape, strideA, strideB, strideOut, opStr, false);

        // Execute the JIT-compiled kernel
        kernel(this.data, b.data, result.data, this.offset, b.offset, result.offset);
        return result;
    }

    _comparisonOp(other, compFn) {
        const b = (typeof other === 'number') ? array([other]) : other;
        const { outShape, strideA, strideB } = broadcastShapes(this, b);
        const result = zeros(outShape, 'uint8');
        
        const strideOut = result.strides;
        const opStr = compFn.toString();

        const cacheKey = `comp|${this.dtype}|${opStr}|${outShape}|${strideA}|${strideB}|${strideOut}`;
        
        let kernel = Jit._createBinKernel(cacheKey, outShape, strideA, strideB, strideOut, opStr, true);

        kernel(this.data, b.data, result.data, this.offset, b.offset, result.offset);
        return result;
    }


}




// --- Broadcasting Helper ---

/**
 * Calculates the resulting shape from broadcasting two shapes and returns the
 * corresponding strides for each input array to match that new shape.
 * @private
 * @param {NDArray} a - First array.
 * @param {NDArray} b - Second array.
 * @returns {{outShape: Int32Array, strideA: Int32Array, strideB: Int32Array}}
 */
function broadcastShapes(a, b) {
    const ndim = Math.max(a.ndim, b.ndim);
    const outShape = new Int32Array(ndim);
    const strideA = new Int32Array(ndim);
    const strideB = new Int32Array(ndim);

    for (let i = 1; i <= ndim; i++) {
        const dimA = a.shape[a.ndim - i] || 1;
        const dimB = b.shape[b.ndim - i] || 1;
        const sA = a.strides[a.ndim - i] || 0;
        const sB = b.strides[b.ndim - i] || 0;

        const outDimI = ndim - i;

        if (dimA === dimB) {
            outShape[outDimI] = dimA;
            strideA[outDimI] = sA;
            strideB[outDimI] = sB;
        } else if (dimA === 1) {
            outShape[outDimI] = dimB;
            strideA[outDimI] = 0; // Stride is 0 for broadcasted dimension
            strideB[outDimI] = sB;
        } else if (dimB === 1) {
            outShape[outDimI] = dimA;
            strideA[outDimI] = sA;
            strideB[outDimI] = 0; // Stride is 0 for broadcasted dimension
        } else {
            throw new Error(`Incompatible shapes for broadcasting: [${a.shape}] and [${b.shape}]`);
        }
    }
    return { outShape, strideA, strideB };
}
