/*-------------------------------------------------------------------------
 * drawElements Quality Program OpenGL ES Utilities
 * ------------------------------------------------
 *
 * Copyright 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

'use strict';
goog.provide('framework.delibs.debase.deMath');

/** @typedef { (Int8Array|Uint8Array|Uint8ClampedArray|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array) } */
goog.TypedArray;

/** @typedef { (Array<number>|Array<boolean>|goog.TypedArray) } */
goog.NumberArray;

goog.scope(function() {

var deMath = framework.delibs.debase.deMath;

var DE_ASSERT = function(x) {
    if (!x)
        throw new Error('Assert failed');
};

/** @const */ deMath.INT32_SIZE = 4;

deMath.deInRange32 = function(a, mn, mx) {
    return (a >= mn) && (a <= mx);
};

deMath.deInBounds32 = function(a, mn, mx) {
    return (a >= mn) && (a < mx);
};

/**
 * @param {number} a
 * @return {number}
 */
deMath.deFloatFrac = function(a) { return a - Math.floor(a); };

/**
 * Transform a 64-bit float number into a 32-bit float number.
 * Native dEQP uses 32-bit numbers, so sometimes 64-bit floating numbers in JS should be transformed into 32-bit ones to ensure the correctness of the result.
 * @param {number} a
 * @return {number}
 */
deMath.toFloat32 = (function() {
    var FLOAT32ARRAY1 = new Float32Array(1);
    return function(a) {
        FLOAT32ARRAY1[0] = a;
        return FLOAT32ARRAY1[0];
    };
})();

/** @const */ deMath.INV_LOG_2_FLOAT32 = deMath.toFloat32(1.44269504089); /** 1.0 / log_e(2.0) */

/**
 * Check if a value is a power-of-two.
 * @param {number} a Input value.
 * @return {boolean} return True if input is a power-of-two value, false otherwise.
 * (Also returns true for zero).
 */
deMath.deIsPowerOfTwo32 = function(a) {
    return ((a & (a - 1)) == 0);
};

/**
 * Align an integer to given power-of-two size.
 * @param {number} val The number to align.
 * @param {number} align The size to align to.
 * @return {number} The aligned value
 */
deMath.deAlign32 = function(val, align) {
    if (!deMath.deIsPowerOfTwo32(align))
        throw new Error('Not a power of 2: ' + align);
    return ((val + align - 1) & ~(align - 1)) & 0xFFFFFFFF; //0xFFFFFFFF make sure it returns a 32 bit calculation in 64 bit browsers.
};

/**
 * Compute the bit population count of an integer.
 * @param {number} a
 * @return {number} The number of one bits in
 */
deMath.dePop32 = function(a) {
    /** @type {number} */ var mask0 = 0x55555555; /* 1-bit values. */
    /** @type {number} */ var mask1 = 0x33333333; /* 2-bit values. */
    /** @type {number} */ var mask2 = 0x0f0f0f0f; /* 4-bit values. */
    /** @type {number} */ var mask3 = 0x00ff00ff; /* 8-bit values. */
    /** @type {number} */ var mask4 = 0x0000ffff; /* 16-bit values. */
    /** @type {number} */ var t = a & 0xFFFFFFFF; /* Crop to 32-bit value */
    t = (t & mask0) + ((t >> 1) & mask0);
    t = (t & mask1) + ((t >> 2) & mask1);
    t = (t & mask2) + ((t >> 4) & mask2);
    t = (t & mask3) + ((t >> 8) & mask3);
    t = (t & mask4) + (t >> 16);
    return t;
};

deMath.clamp = function(val, minParm, maxParm) {
    return Math.min(Math.max(val, minParm), maxParm);
};

/**
 * @param {Array<number>} values
 * @param {number} minParm
 * @param {number} maxParm
 * @return {Array<number>}
 */
deMath.clampVector = function(values, minParm, maxParm) {
    var result = [];
    for (var i = 0; i < values.length; i++)
        result.push(deMath.clamp(values[i], minParm, maxParm));
    return result;
};

deMath.imod = function(a, b) {
    var m = a % b;
    return m < 0 ? m + b : m;
};

deMath.mirror = function(a) {
    return a >= 0 ? a : -(1 + a);
};

/**
 * @param {goog.NumberArray} a Source array
 * @param {goog.NumberArray} indices
 * @return {Array<number>} Swizzled array
 */
deMath.swizzle = function(a, indices) {
    if (!indices.length)
        throw new Error('Argument must be an array');
    var dst = [];
    for (var i = 0; i < indices.length; i++)
        dst.push(a[indices[i]]);
    return dst;
};

/**
 * Shift left elements of array a by elements of array b
 * dst[n] a[n] << b[n]
 * @param {goog.NumberArray} a
 * @param {goog.NumberArray} b
 * @return {Array<number>} Result array
 */
deMath.arrayShiftLeft = function(a, b) {
    if (a.length != b.length)
        throw new Error('Arrays must have the same size');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(a[i] << b[i]);
    return dst;
};

/**
 * Multiply two vectors, element by element
 * @param {goog.NumberArray} a
 * @param {goog.NumberArray} b
 * @return {Array<number>} Result array
 */

deMath.multiply = function(a, b) {
    if (a.length != b.length)
        throw new Error('Arrays must have the same size');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(a[i] * b[i]);
    return dst;
};

/**
 * Divide two vectors, element by element
 * @param {goog.NumberArray} a
 * @param {goog.NumberArray} b
 * @return {Array<number>} Result array
 * @throws {Error}
 */

deMath.divide = function(a, b) {
    if (a.length != b.length)
        throw new Error('Arrays must have the same size');
    var dst = [];
    for (var i = 0; i < a.length; i++) {
        if (b[i] === 0)
            throw new Error('Division by 0');
        dst.push(a[i] / b[i]);
    }
    return dst;
};

/**
 * Divide vector by a scalar
 * @param {goog.NumberArray} a
 * @param {number} b
 * @return {Array<number>} Result array
 */
deMath.divideScale = function(a, b) {
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(a[i] / b);
    return dst;
};

/**
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
deMath.mod = function(a, b) {
    return a - b * Math.floor(a / b);
};

/**
 * Modulus vector by a scalar
 * @param {goog.NumberArray} a
 * @param {number} b
 * @return {Array<number>} Result array
 */
deMath.modScale = function(a, b) {
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(deMath.mod(a[i], b));
    return dst;
};

/**
 * Multiply vector by a scalar
 * @param {goog.NumberArray} a
 * @param {number} b
 * @return {Array<number>} Result array
 */
deMath.scale = function(a, b) {
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(a[i] * b);
    return dst;
};

/**
 * Add vector and scalar, element by element
 * @param {goog.NumberArray} a
 * @param {number} b
 * @return {Array<number>} Result array
 */
deMath.addScalar = function(a, b) {
    if (!Array.isArray(a))
        throw new Error('First argument must be an array.');
    if (typeof b !== 'number')
        throw new Error('Second argument must be a number.');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(a[i] + b);
    return dst;
};

/**
 * Add two vectors, element by element
 * @param {goog.NumberArray} a
 * @param {goog.NumberArray} b
 * @return {Array<number>} Result array
 */
deMath.add = function(a, b) {
    if (a.length != b.length)
        throw new Error('Arrays must have the same size');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(a[i] + b[i]);
    return dst;
};

/**
 * Subtract two vectors, element by element
 * @param {goog.NumberArray} a
 * @param {goog.NumberArray} b
 * @return {Array<number>} Result array
 */

deMath.subtract = function(a, b) {
    if (a.length != b.length)
        throw new Error('Arrays must have the same size');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(a[i] - b[i]);
    return dst;
};

/**
 * Subtract vector and scalar, element by element
 * @param {goog.NumberArray} a
 * @param {number} b
 * @return {Array<number>} Result array
 */
deMath.subScalar = function(a, b) {
    if (!Array.isArray(a))
        throw new Error('First argument must be an array.');
    if (typeof b !== 'number')
        throw new Error('Second argument must be a number.');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(a[i] - b);
    return dst;
};

/**
 * Calculate absolute difference between two vectors
 * @param {goog.NumberArray} a
 * @param {goog.NumberArray} b
 * @return {Array<number>} abs(diff(a - b))
 */
deMath.absDiff = function(a, b) {
    if (a.length != b.length)
        throw new Error('Arrays must have the same size');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(Math.abs(a[i] - b[i]));
    return dst;
};

/**
 * Calculate absolute value of a vector
 * @param {goog.NumberArray} a
 * @return {Array<number>} abs(a)
 */
deMath.abs = function(a) {
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(Math.abs(a[i]));
    return dst;
};

/**
 * Is a <= b (element by element)?
 * @param {goog.NumberArray} a
 * @param {goog.NumberArray} b
 * @return {Array<boolean>} Result array of booleans
 */
deMath.lessThanEqual = function(a, b) {
    if (a.length != b.length)
        throw new Error('Arrays must have the same size');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(a[i] <= b[i]);
    return dst;
};

/**
 * Is a === b (element by element)?
 * @param {goog.NumberArray} a
 * @param {goog.NumberArray} b
 * @return {boolean} Result
 */
deMath.equal = function(a, b) {
    if (a.length != b.length)
        throw new Error('Arrays must have the same size');
    for (var i = 0; i < a.length; i++) {
        if (a[i] !== b[i])
            return false;
    }
    return true;
};

/**
 * Are all values in the array true?
 * @param {Array<boolean>} a
 * @return {boolean}
 */
deMath.boolAll = function(a) {
    for (var i = 0; i < a.length; i++)
        if (a[i] == false)
            return false;
    return true;
};

/**
 * deMath.assign(a, b) element by element
 * @param {goog.NumberArray} a
 * @return {Array<number>}
 */
deMath.assign = function(a) {
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(a[i]);
    return dst;
};

/**
 * deMath.max(a, b) element by element
 * @param {goog.NumberArray} a
 * @param {goog.NumberArray} b
 * @return {Array<number>}
 */
deMath.max = function(a, b) {
    if (a.length != b.length)
        throw new Error('Arrays must have the same size');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(Math.max(a[i], b[i]));
    return dst;
};

/**
 * deMath.min(a, b) element by element
 * @param {goog.NumberArray} a
 * @param {goog.NumberArray} b
 * @return {Array<number>}
 */
deMath.min = function(a, b) {
    if (a.length != b.length)
        throw new Error('Arrays must have the same size');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(Math.min(a[i], b[i]));
    return dst;
};

// Nearest-even rounding in case of tie (fractional part 0.5), otherwise ordinary rounding.
deMath.rint = function(a) {
    var floorVal = Math.floor(a);
    var fracVal = a - floorVal;

    if (fracVal != 0.5)
        return Math.round(a); // Ordinary case.

    var roundUp = (floorVal % 2) != 0;

    return floorVal + (roundUp ? 1 : 0);
};

/**
 * wrap the number, so that it fits in the range [minValue, maxValue]
 * @param {number} v
 * @param {number} minValue
 * @param {number} maxValue
 * @return {number}
 */
deMath.wrap = function(v, minValue, maxValue) {
    var range = maxValue - minValue + 1;

    if (v < minValue) {
        v += range * (Math.floor((minValue - v) / range) + 1);
    }
    return minValue + Math.floor((v - minValue) % range);
};

/**
 * Round number to int by dropping fractional part
 * it is equivalent of GLSL int() constructor
 * @param {number} a
 * @return {number}
 */
deMath.intCast = function(a) {
    var v;
    if (a >= 0)
        v = Math.floor(a);
    else
        v = Math.ceil(a);
    return deMath.wrap(v, -0x80000000, 0x7FFFFFFF);
};

/**
 * Round number to uint by dropping fractional part
 * it is equivalent of GLSL uint() constructor
 * @param {number} a
 * @return {number}
 */
deMath.uintCast = function(a) {
    var v;
    if (a >= 0)
        v = Math.floor(a);
    else
        v = Math.ceil(a);
    return deMath.wrap(v, 0, 0xFFFFFFFF);
};

/**
 * @param {number} a
 * @return {number}
 */
deMath.logToFloor = function(a) {
    assertMsgOptions(a > 0, 'Value is less or equal than zero', false, true);
    return 31 - deMath.clz32(a);
};

/**
 * Find intersection of two rectangles
 * @param {goog.NumberArray} a Array [x, y, width, height]
 * @param {goog.NumberArray} b Array [x, y, width, height]
 * @return {Array<number>}
 */
deMath.intersect = function(a, b) {
    if (a.length != 4)
        throw new Error('Array "a" must have length 4 but has length: ' + a.length);
    if (b.length != 4)
        throw new Error('Array "b" must have length 4 but has length: ' + b.length);
    var x0 = Math.max(a[0], b[0]);
    var y0 = Math.max(a[1], b[1]);
    var x1 = Math.min(a[0] + a[2], b[0] + b[2]);
    var y1 = Math.min(a[1] + a[3], b[1] + b[3]);
    var w = Math.max(0, x1 - x0);
    var h = Math.max(0, y1 - y0);

    return [x0, y0, w, h];
};

/** deMath.deMathHash
 * @param {number} a
 * @return {number}
 */
deMath.deMathHash = function(a) {
    var key = a;
    key = (key ^ 61) ^ (key >> 16);
    key = key + (key << 3);
    key = key ^ (key >> 4);
    key = key * 0x27d4eb2d; /* prime/odd constant */
    key = key ^ (key >> 15);
    return key;
};

/**
 * Converts a byte array to a number. Cannot convert numbers larger than 52 bits.
 * @param {Uint8Array} array
 * @return {number}
 */
deMath.arrayToNumber = function(array) {
    DE_ASSERT(array.length <= 6 || (array.length == 6 && array[5] <= 127));
    /** @type {number} */ var result = 0;

    for (var ndx = 0; ndx < array.length; ndx++) {
        result += array[ndx] * Math.pow(256, ndx);
    }

    return result;
};

/**
 * Fills a byte array with a number
 * @param {Uint8Array} array Output array (already resized)
 * @param {number} number
 */
deMath.numberToArray = function(array, number) {
    DE_ASSERT(Number.isInteger(number));
    for (var byteNdx = 0; byteNdx < array.length; byteNdx++) {
        /** @type {number} */ var acumzndx = !byteNdx ? number : Math.floor(number / Math.pow(256, byteNdx));
        array[byteNdx] = acumzndx & 0xFF;
    }
};

/**
 * Obtains the bit fragment from a number
 * @param {number} x
 * @param {number} firstNdx
 * @param {number} lastNdx
 * @return {number}
 */
deMath.getBitRange = function(x, firstNdx, lastNdx) {
    DE_ASSERT(lastNdx - firstNdx <= 52);
    var shifted = deMath.shiftRight(x, firstNdx);
    var bitSize = lastNdx - firstNdx;
    var mask;
    if (bitSize < 32)
        mask = (1 << bitSize) - 1;
    else
        mask = Math.pow(2, bitSize) - 1;
    var masked = deMath.binaryAnd(shifted, mask);
    return masked;
};

/**
 * Obtains the bit fragment from a Uint32Array representing a number.
 * (ArrayBuffer representations are used in tcuFloat.)
 *
 * Cannot return more than 52 bits ((lastNdx - firstNdx) <= 52).
 *
 * @param {Uint32Array} array
 * @param {number} firstNdx
 * @param {number} lastNdx
 * @return {number}
 */
deMath.getArray32BitRange = function(array, firstNdx, lastNdx) {
    DE_ASSERT(0 <= firstNdx && firstNdx < (array.length * 32));
    DE_ASSERT(0 < lastNdx && lastNdx <= (array.length * 32));
    DE_ASSERT((lastNdx - firstNdx) <= 52);

    // Example of how this works for a 64-bit number (Uint32Array of length 2).
    //
    // * Note that the shift operators in the code << and >>> are pointing in
    //   the opposite direction of this diagram, since LSB is shown on the left.
    //
    // [array[0],                         array[1]                        ]
    // [00000011111111111111111111111111, 11111111111100000000000000000000]
    //  ^LSB                        MSB^  ^LSB                        MSB^
    //
    // [00000011111111111111111111111111, 11111111111100000000000000000000]
    //        \                                       \
    //         firstNdx = 6 (inclusive)                lastNdx = 44 (exclusive)
    //         blockIndexA = 0                         blockIndexB = 1
    //
    // [00000011111111111111111111111111, 11111111111100000000000000000000]
    //  \-----\                                       \-------------------\
    //   bitsToBeginningOfBlock = 6                    bitsFromEndOfBlock = 20
    //
    //  -------------blockA-------------  -------------blockB-------------
    // [00000011111111111111111111111111, 11111111111100000000000000000000]
    //        ^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^
    //                   blockATruncated  blockBTruncated
    //        \--blockATruncatedLength--\
    //
    //        11111111111111111111111111  111111111111
    //        ^^^^^^^^^^^^^^^^^^^^^^^^^^--^^^^^^^^^^^^   return value (38 bits)

    /** @type {number} */ var blockIndexA = Math.floor(firstNdx / 32);
    /** @type {number} */ var bitsToBeginningOfBlock = firstNdx % 32;
    /** @type {number} */ var blockIndexB = Math.floor((lastNdx - 1) / 32);
    /** @type {number} */ var bitsFromEndOfBlock = 31 - ((lastNdx - 1) % 32);

    /** @type {number} */ var blockB = array[blockIndexB];
    // Chop off the most significant `bitsFromEndOfBlock` bits from blockB.
    // Note: Initially this logic used a bitmask instead. But there are subtle
    //   corner cases in JS that caused results to sometimes come out negative.
    //   This truncation method is just used to avoid that complexity.
    /** @type {number} */ var blockBTruncated = (blockB << bitsFromEndOfBlock) >>> bitsFromEndOfBlock;

    if (blockIndexA == blockIndexB) {
        // firstNdx and lastNdx are in the same block.
        // Chop off the least significant `bitsToBeginningOfBlock` bits from blockBTruncated.
        return blockBTruncated >>> bitsToBeginningOfBlock;
    } else {
        // firstNdx and lastNdx are in different blocks.
        /** @type {number} */ var blockA = array[blockIndexA];
        // Chop off the least significant `bitsToBeginningOfBlock` bits from blockA.
        /** @type {number} */ var blockATruncated = blockA >>> bitsToBeginningOfBlock;
        /** @type {number} */ var blockATruncatedLength = 32 - bitsToBeginningOfBlock;

        // Concatenate blockATruncated and blockBTruncated.
        // Conceptually equivalent to:
        //     blockATruncated | (blockBTruncated << blockATruncatedLength)
        // except that wouldn't work for numbers larger than 32 bits.
        return blockATruncated + (blockBTruncated * Math.pow(2, blockATruncatedLength));
    }
};

/**
 * Split a large signed number into low and high 32bit dwords.
 * @param {number} x
 * @return {Array<number>}
 */
deMath.split32 = function(x) {
    var ret = [];
    ret[1] = Math.floor(x / 0x100000000);
    ret[0] = x - ret[1] * 0x100000000;
    return ret;
};

/**
 * Split a signed number's low 32bit dwords into low and high 16bit dwords.
 * @param {number} x
 * @return {Array<number>}
 */
deMath.split16 = function(x) {
    var ret = [];
    x = x & 0xffffffff;
    ret[1] = Math.floor(x / 0x10000);
    ret[0] = x - ret[1] * 0x10000;
    return ret;
};

/**
 * Recontruct a number from high and low 32 bit dwords
 * @param {Array<number>} x
 * @return {number}
 */
deMath.join32 = function(x) {
    var v0 = x[0] >= 0 ? x[0] : 0x100000000 + x[0];
    var v1 = x[1];
    var val = v1 * 0x100000000 + v0;
    return val;
};

//Bit operations with the help of arrays

/**
 * @enum
 */
deMath.BinaryOp = {
    AND: 0,
    OR: 1,
    XOR: 2
};

/**
 * Performs a normal (native) binary operation
 * @param {number} valueA First operand
 * @param {number} valueB Second operand
 * @param {deMath.BinaryOp} operation The desired operation to perform
 * @return {number}
 */
deMath.doNativeBinaryOp = function(valueA, valueB, operation) {
    switch (operation) {
        case deMath.BinaryOp.AND:
            return valueA & valueB;
        case deMath.BinaryOp.OR:
            return valueA | valueB;
        case deMath.BinaryOp.XOR:
            return valueA ^ valueB;
        default:
            throw new Error('Unknown operation: ' + operation);
    }
};

/**
 * Performs a binary operation between two operands
 * with the help of arrays to avoid losing the internal binary representation.
 * @param {number} valueA First operand
 * @param {number} valueB Second operand
 * @param {deMath.BinaryOp} binaryOpParm The desired operation to perform
 * @return {number}
 */
deMath.binaryOp = function(valueA, valueB, binaryOpParm) {
    //quick path if values fit in signed 32 bit range
    if (deMath.deInRange32(valueA, -0x80000000, 0x7FFFFFFF) && deMath.deInRange32(valueB, -0x80000000, 0x7FFFFFFF))
        return deMath.doNativeBinaryOp(valueA, valueB, binaryOpParm);

    var x = deMath.split32(valueA);
    var y = deMath.split32(valueB);
    var z = [];
    for (var i = 0; i < 2; i++)
        z[i] = deMath.doNativeBinaryOp(x[i], y[i], binaryOpParm);
    var ret = deMath.join32(z);
    return ret;
};

/**
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
deMath.binaryAnd = function(a, b) {
    return deMath.binaryOp(a, b, deMath.BinaryOp.AND);
};

/**
 * @param {goog.NumberArray} a
 * @param {number} b
 * @return {Array<number>}
 */
deMath.binaryAndVecScalar = function(a, b) {
    if (!Array.isArray(a))
        throw new Error('First argument must be an array.');
    if (typeof b !== 'number')
        throw new Error('Second argument must be a number.');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(deMath.binaryOp(a[i], b, deMath.BinaryOp.AND));
    return dst;
};

/**
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
deMath.binaryOr = function(a, b) {
    return deMath.binaryOp(a, b, deMath.BinaryOp.OR);
};

/**
 * @param {goog.NumberArray} a
 * @param {number} b
 * @return {Array<number>}
 */
deMath.binaryOrVecScalar = function(a, b) {
    if (!Array.isArray(a))
        throw new Error('First argument must be an array.');
    if (typeof b !== 'number')
        throw new Error('Second argument must be a number.');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(deMath.binaryOp(a[i], b, deMath.BinaryOp.OR));
    return dst;
};

/**
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
deMath.binaryXor = function(a, b) {
    return deMath.binaryOp(a, b, deMath.BinaryOp.XOR);
};

/**
 * @param {goog.NumberArray} a
 * @param {number} b
 * @return {Array<number>}
 */
deMath.binaryXorVecScalar = function(a, b) {
    if (!Array.isArray(a))
        throw new Error('First argument must be an array.');
    if (typeof b !== 'number')
        throw new Error('Second argument must be a number.');
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(deMath.binaryOp(a[i], b, deMath.BinaryOp.XOR));
    return dst;
};

/**
 * Performs a binary NOT operation on an operand
 * @param {number} value Operand
 * @return {number}
 */
deMath.binaryNot = function(value) {
    //quick path if value fits in signed 32 bit range
    if (deMath.deInRange32(value, -0x80000000, 0x7FFFFFFF))
        return ~value;

    var x = deMath.split32(value);
    x[0] = ~x[0];
    x[1] = ~x[1];
    var ret = deMath.join32(x);
    return ret;
};

/**
 * Shifts the given value 'steps' bits to the left. Replaces << operator
 * This function should be used if the expected value will be wider than 32-bits.
 * @param {number} value
 * @param {number} steps
 * @return {number}
 */
deMath.shiftLeft = function(value, steps) {
    //quick path
    if (steps < 31) {
        var v = value * (1 << steps);
        if (deMath.deInRange32(v, -0x80000000, 0x7FFFFFFF))
            return v;
    }

    if (steps == 0)
        return value;
    else if (steps < 32) {
        var mask = (1 << 32 - steps) - 1;
        var x = deMath.split32(value);
        var highBits = x[0] & (~mask);
        var y = highBits >> (32 - steps);
        if (highBits < 0) {
            var m = (1 << steps) - 1;
            y &= m;
        }
        var result = [];
        result[0] = x[0] << steps;
        result[1] = x[1] << steps;
        result[1] |= y;

        return deMath.join32(result);
    } else {
        var x = deMath.split32(value);
        var result = [];
        result[0] = 0;
        result[1] = x[0] << steps - 32;
        return deMath.join32(result);
    }
};

/**
 * @param {Array<number>} a
 * @param {number} b
 */
deMath.shiftLeftVecScalar = function(a, b) {
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(deMath.shiftLeft(a[i], b));
    return dst;
};

/**
 * Shifts the given value 'steps' bits to the right. Replaces >> operator
 * This function should be used if the value is wider than 32-bits
 * @param {number} value
 * @param {number} steps
 * @return {number}
 */
deMath.shiftRight = function(value, steps) {
    //quick path
    if (deMath.deInRange32(value, -0x80000000, 0x7FFFFFFF) && steps < 32)
        return value >> steps;

    if (steps == 0)
        return value;
    else if (steps < 32) {
        if (steps == 0)
            return value;
        var mask = (1 << steps) - 1;
        var x = deMath.split32(value);
        var lowBits = x[1] & mask;
        var result = [];
        var m = (1 << 32 - steps) - 1;
        result[0] = (x[0] >> steps) & m;
        result[1] = x[1] >> steps;
        result[0] |= lowBits << 32 - steps;
        return deMath.join32(result);
    } else {
        var x = deMath.split32(value);
        var result = [];
        result[0] = x[1] >> steps - 32;
        result[1] = value < 0 ? -1 : 0;
        return deMath.join32(result);
    }
};

/**
 * @param {Array<number>} a
 * @param {number} b
 */
deMath.shiftRightVecScalar = function(a, b) {
    var dst = [];
    for (var i = 0; i < a.length; i++)
        dst.push(deMath.shiftRight(a[i], b));
    return dst;
};

/** deMath.logicalAndBool over two arrays of booleans
 * @param {Array<boolean>} a
 * @param {Array<boolean>} b
 * @return {Array<boolean>}
 */
deMath.logicalAndBool = function(a, b) {
    if (!Array.isArray(a))
        throw new Error('The first parameter is not an array: (' + typeof(a) + ')' + a);
    if (!Array.isArray(b))
        throw new Error('The second parameter is not an array: (' + typeof(b) + ')' + b);
    if (a.length != b.length)
        throw new Error('The lengths of the passed arrays are not equivalent. (' + a.length + ' != ' + b.length + ')');

    /** @type {Array<boolean>} */ var result = [];
    for (var i = 0; i < a.length; i++) {
        if (a[i] & b[i])
            result.push(true);
        else
            result.push(false);
    }
    return result;
};

/** deMath.logicalOrBool over two arrays of booleans
 * @param {Array<boolean>} a
 * @param {Array<boolean>} b
 * @return {Array<boolean>}
 */
deMath.logicalOrBool = function(a, b) {
    if (!Array.isArray(a))
        throw new Error('The first parameter is not an array: (' + typeof(a) + ')' + a);
    if (!Array.isArray(b))
        throw new Error('The second parameter is not an array: (' + typeof(b) + ')' + b);
    if (a.length != b.length)
        throw new Error('The lengths of the passed arrays are not equivalent. (' + a.length + ' != ' + b.length + ')');

    /** @type {Array<boolean>} */ var result = [];
    for (var i = 0; i < a.length; i++) {
        if (a[i] | b[i])
            result.push(true);
        else
            result.push(false);
    }
    return result;
};

/** deMath.logicalNotBool over an array of booleans
 * @param {Array<boolean>} a
 * @return {Array<boolean>}
 */
deMath.logicalNotBool = function(a) {
    if (!Array.isArray(a))
        throw new Error('The passed value is not an array: (' + typeof(a) + ')' + a);

    /** @type {Array<boolean>} */ var result = [];
    for (var i = 0; i < a.length; i++)
        result.push(!a[i]);
    return result;
};

/** deMath.greaterThan over two arrays of booleans
 * @param {Array<number>} a
 * @param {Array<number>} b
 * @return {Array<boolean>}
 */
deMath.greaterThan = function(a, b) {
    if (!Array.isArray(a))
        throw new Error('The first parameter is not an array: (' + typeof(a) + ')' + a);
    if (!Array.isArray(b))
        throw new Error('The second parameter is not an array: (' + typeof(b) + ')' + b);
    if (a.length != b.length)
        throw new Error('The lengths of the passed arrays are not equivalent. (' + a.length + ' != ' + b.length + ')');

    /** @type {Array<boolean>} */ var result = [];
    for (var i = 0; i < a.length; i++)
        result.push(a[i] > b[i]);
    return result;
};

/** deMath.greaterThan over two arrays of booleans
 * @param {Array<number>} a
 * @param {Array<number>} b
 * @return {Array<boolean>}
 */
deMath.greaterThanEqual = function(a, b) {
    if (!Array.isArray(a))
        throw new Error('The first parameter is not an array: (' + typeof(a) + ')' + a);
    if (!Array.isArray(b))
        throw new Error('The second parameter is not an array: (' + typeof(b) + ')' + b);
    if (a.length != b.length)
        throw new Error('The lengths of the passed arrays are not equivalent. (' + a.length + ' != ' + b.length + ')');

    /** @type {Array<boolean>} */ var result = [];
    for (var i = 0; i < a.length; i++)
        result.push(a[i] >= b[i]);
    return result;
};

/**
 * Array of float to array of int (0, 255)
 * @param {Array<number>} a
 * @return {Array<number>}
 */

deMath.toIVec = function(a) {
    /** @type {Array<number>} */ var res = [];
    for (var i = 0; i < a.length; i++)
        res.push(deMath.clamp(Math.floor(a[i] * 255), 0, 255));
    return res;
};

/**
 * @param {number} a
 * @return {number}
 */
 deMath.clz32 = function(a) {
   /** @type {number} */ var maxValue = 2147483648; // max 32 bit number
   /** @type {number} */ var leadingZeros = 0;
   while (a < maxValue) {
     maxValue = maxValue >>> 1;
     leadingZeros++;
   }
   return leadingZeros;
};

/**
 * @param {number} a
 * @param {number} exponent
 * @return {number}
 */
deMath.deLdExp = function(a, exponent) {
    return deMath.ldexp(a, exponent);
};

/**
 * @param {number} a
 * @param {number} exponent
 * @return {number}
 */
deMath.deFloatLdExp = function(a, exponent) {
    return deMath.ldexp(a, exponent);
};

/**
 * @param {number} value
 * @return {Array<number>}
 */
deMath.frexp = (function() {
   var data = new DataView(new ArrayBuffer(8));

   return function(value) {
       if (value === 0) return [value, 0];
       data.setFloat64(0, value);
       var bits = (data.getUint32(0) >>> 20) & 0x7FF;
       if (bits === 0) {
           data.setFloat64(0, value * Math.pow(2, 64));
           bits = ((data.getUint32(0) >>> 20) & 0x7FF) - 64;
       }
       var exponent = bits - 1022,
           mantissa = deMath.ldexp(value, -exponent);
       return [mantissa, exponent];
   }
})();

/**
 * @param {number} mantissa
 * @param {number} exponent
 * @return {number}
 */
deMath.ldexp = function(mantissa, exponent) {
    return exponent > 1023 ? // avoid multiplying by infinity
            mantissa * Math.pow(2, 1023) * Math.pow(2, exponent - 1023) :
            exponent < -1074 ? // avoid multiplying by zero
            mantissa * Math.pow(2, -1074) * Math.pow(2, exponent + 1074) :
            mantissa * Math.pow(2, exponent);
};

/**
 * @param {number} a
 * @return {number}
 */
deMath.deCbrt = function(a) {
    return deMath.deSign(a) * Math.pow(Math.abs(a), 1.0 / 3.0);
};

/**
 * @param {number} x
 * @return {number}
 */
deMath.deSign = function(x) {
    return isNaN(x) ? x : ((x > 0.0) - (x < 0.0));
};

deMath.deFractExp = function(x) {
    var result = {
        significand: x,
        exponent: 0
    };

    if (isFinite(x)) {
        var r = deMath.frexp(x);
        result.exponent = r[1] - 1;
        result.significand = r[0] * 2;
    }
    return result;
};

});
