Source: hat.js

'use strict';
const HatByteProcessor = require('./hat-processor').HatByteProcessor;


/**
 * A Sorting Hat
 * @class
 * @param {ArrayBuffer} arrayBuffer - allocate this Hat with some working memory
 * @throws {TypeError} if arrayBuffer is not an instance of ArrayBuffer
 */
function Hat(arrayBuffer) {
    if (!(arrayBuffer instanceof ArrayBuffer)) {
        throw new TypeError('argument not an instance of ArrayBuffer');
    }
    this.arrayBuffer = arrayBuffer;
    this.dataView = new DataView(this.arrayBuffer);
}


/**
 * Compare two hat instances on data contents
 *
 * @param {Hat} hat - instance of <b>a</b> Hat
 * @returns {boolean} if instance (or contents) are the same
 * @throws {TypeError} if hat is not an instance of Hat
 */
Hat.prototype.compare = function (hat) {
    let same = false;
    if (!(hat instanceof Hat)) {
        throw new TypeError('hat is not an instance of Hat');
    }
    const arrayBuffer = hat.arrayBuffer;
    const thisArrayBuffer = this.arrayBuffer;

    if (thisArrayBuffer === arrayBuffer) {
        same = true;
    } else {
        const thisDataView = this.dataView;
        const dataView = hat.dataView;
        const byteLength = Math.max(thisArrayBuffer.byteLength, arrayBuffer.byteLength);
        let result = true;
        for (let i = 0; i < byteLength && result; i++) {
            const value1 = (i < thisDataView.byteLength) ? thisDataView.getUint8(i) : 0x0;
            const value2 = (i < dataView.byteLength) ? dataView.getUint8(i) : 0x0;
            if (value1 !== value2) {
                result = false;
            }
        }
        same = result;
    }
    return same;
};

/**
 * This method analyzes every bit value in this hat and creates the corresponding <br>
 *     position array where bits are logical true.
 *
 * @example
 *
 *
 *   // Initialize a Hat with ArrayBuffer (size = 2 bytes)
 *   const ab = new ArrayBuffer(2);
 *   const hat = new Hat(ab);
 *
 *   // Pre set some bit positions
 *   hat.setPosition(1)
 *   .setPosition(2)
 *   .setPosition(3)
 *   .setPosition(4)
 *   .setPosition(5)
 *   .setPosition(6)
 *   .setPosition(7)
 *   .setPosition(8)
 *   .setPosition(10);
 *
 *   hat.getPositionsArray(); // returns: [1, 2, 3, 4, 5, 6, 7, 8, 10]
 *
 *
 * @returns {Array} Which contains bit positions from this hat, which are logical set to true
 */
Hat.prototype.getPositionsArray = function () {
    const dataView = this.dataView;
    const byteLength = this.arrayBuffer.byteLength;
    const positions = [];
    for (let byteNo = 0; byteNo < byteLength; byteNo++) {
        const value = dataView.getUint8(byteNo);
        for (let bitPos = 0; bitPos < 8; bitPos++) {
            const posValue = HatByteProcessor.isTrue(value, bitPos);
            if (posValue) {
                positions.push(byteNo * 8 + bitPos + 1);
            }
        }
    }
    return positions;
};

/**
 * Set bit: true according to integer position in the Sorting Hat <br>
 * <i>Note: Starting from 1</i>
 * @throws {Error} Position can't exceed data view bounds.
 * @param {Number} position - unsigned integer value
 * @returns {Hat} - For chaining purposes
 */
Hat.prototype.setPosition = function (position) {
    const bitBytePosition = HatByteProcessor.getByteNoAndBitPos(position);
    const dataViewBounds = this.dataView.byteLength;
    if (bitBytePosition.byteNo < dataViewBounds) {
        let byte = this.dataView.getUint8(bitBytePosition.byteNo);
        byte = HatByteProcessor.setBit(byte, bitBytePosition.bitPos);
        this.dataView.setUint8(bitBytePosition.byteNo, byte);
    } else {
        // Because we can't exceed the amount of allocated bytes,
        // Please ensure position ends within the allocated memory
        throw new Error('Illegal position');
    }
    return this;
};

/**
 * Set bit: false according to integer position in the Sorting Hat
 * Note: Starting from 1
 * @param {Number} position - unsigned integer value
 * @returns {Hat}
 */
Hat.prototype.clearPosition = function (position) {
    const bitBytePosition = HatByteProcessor.getByteNoAndBitPos(position);
    let byte = this.dataView.getUint8(bitBytePosition.byteNo);
    byte = HatByteProcessor.clearBit(byte, bitBytePosition.bitPos);
    this.dataView.setUint8(bitBytePosition.byteNo, byte);
    return this;
};

/**
 * Toggle bit value true => false, false => true
 * @example
 *
 * const hat = new Hat(ArrayBuffer(2));
 * hat.changePosition(1, true);
 *
 * hat.isPosition(1); // true
 *
 * @param {Number} position - unsigned integer value
 * @returns {Hat}
 */
Hat.prototype.togglePosition = function (position) {
    const bitBytePosition = HatByteProcessor.getByteNoAndBitPos(position);
    let byte = this.dataView.getUint8(bitBytePosition.byteNo);
    byte = HatByteProcessor.toggleBit(byte, bitBytePosition.bitPos);
    this.dataView.setUint8(bitBytePosition.byteNo, byte);
    return this;
};

/**
 * Set value for a bit on position
 *
 * @example
 *
 * const hat = new Hat(ArrayBuffer(2));
 * hat.changePosition(1, true);
 *
 * hat.isPosition(1); // returns true
 *
 *
 * @param {Number} position - unsigned integer value
 * @param {boolean} [on] - Optional set true or false (default : false)
 * @returns {Hat} - Return itself for chaining purposes
 */
Hat.prototype.changePosition = function (position, on = false) {
    const bitBytePosition = HatByteProcessor.getByteNoAndBitPos(position);
    let byte = this.dataView.getUint8(bitBytePosition.byteNo);
    byte = HatByteProcessor.changeBit(byte, bitBytePosition.bitPos, on);
    this.dataView.setUint8(bitBytePosition.byteNo, byte);
    return this;
};

/**
 * Checks if a value is true or false on a specific position
 *
 *
 * @example
 *
 * const hat = new Hat(ArrayBuffer(2));
 * hat.setPosition(1);
 *
 * hat.isPosition(1); // returns true
 *
 * @param {Number} position - unsigned integer value
 * @returns {boolean} if membership is set
 */
Hat.prototype.isPosition = function (position) {
    const bitBytePosition = HatByteProcessor.getByteNoAndBitPos(position);
    let byte = this.dataView.getUint8(bitBytePosition.byteNo);
    const result = HatByteProcessor.isTrue(byte, bitBytePosition.bitPos);
    return result;
};

/**
 * Compares all given positions
 * - A positive position means this position should be set truthy
 * - A negative position means this position should be set falsy
 *
 *  When wants to check on bit positions outside the memory bounds (dataViewBounds), <br>
 *  method wil return early with falsy result
 *
 *
 * @example
 *
 *  // Set 2 bit positions to logical '1'
 *  const hat = new Hat(new ArrayBuffer(2));
 *  hat.setPosition(1).setPosition(2);
 *
 *  hat.hasAllFromPositions([1, 2]); // true
 *
 * @example
 *
 *  // Set 2 bit positions to logical '1' then the first bit position back to '0'
 *  const hat = new Hat(new ArrayBuffer(2));
 *  hat.setPosition(1).setPosition(2),togglePosition(1);
 *
 *  hat.hasAllFromPositions([-1, 2]); // true
 *
 * @param {Array<Number>} positionArray - containing signed integer values (representing bit positions)
 * @return {boolean} true when all positions correspondent to the given indexes
 * @throws {TypeError} if positionArray is not an instance of array
 *
 */
Hat.prototype.hasAllFromPositions = function (positionArray = []) {
    const self = this;
    const dataViewBounds = self.dataView.byteLength;

    if (!(Array.isArray(positionArray))) {
        throw new TypeError('positionArray not an instance of Array');
    }

    // Shallow copy
    let positions = positionArray.slice();

    // Remove duplicate values:
    positions = positions.filter(function (value, index, self) {
        return self.indexOf(value) === index;
    });

    // Check bits on position if they are truthy (positive position) or falsy (negative position)
    let hasAllFromPositions = true;
    // Exit early when one of the positions is not logic true.
    for (let i = 0; i < positions.length && hasAllFromPositions; i++) {
        const position = positions[i];
        const positionAbs = Math.abs(position);
        const bitBytePosition = HatByteProcessor.getByteNoAndBitPos(positionAbs);
        // Check if position is not out of bound of the data view:
        if (bitBytePosition.byteNo < dataViewBounds) {
            const byte = self.dataView.getUint8(bitBytePosition.byteNo);
            if (position >= 0) {
                hasAllFromPositions = HatByteProcessor.isTrue(byte, bitBytePosition.bitPos);
            } else {
                hasAllFromPositions = HatByteProcessor.isFalse(byte, bitBytePosition.bitPos);
            }

        } else {
            // Position is outside the bounds of the DataView, skipping the rest...
            hasAllFromPositions = false;
        }
    }

    return hasAllFromPositions;
};

/**
 * Able to manipulate bits according to an array of signed integers
 *
 * @example
 *
 *  // Set 2 bit positions to logical '1'
 *  const hat = Hat.create(2);
 *  hat.setAllFromPositions([1, 2]);
 *  hat.hasAllFromPositions([1, 2]); // returns true
 *
 * @example
 *
 *  // Set 1 bit positions to logical '1' and the second to '0'
 *  const hat = Hat.create(2);
 *  hat.setAllFromPositions([1, -2]);
 *  hat.hasAllFromPositions([1]); // returns true
 *
 * @param {Array<Number>} positionArray - Array with integer values starting from 1.
 * @returns {Hat} - For chaining purposes
 * @throws {TypeError} if positionArray is not an instance of array
 */
Hat.prototype.setAllFromPositions = function (positionArray = []) {
    const self = this;
    const dataViewBounds = self.dataView.byteLength;

    if (!(Array.isArray(positionArray))) {
        throw new TypeError('positionArray not an instance of Array');
    }

    // Shallow copy
    let positions = positionArray.slice();

    // Remove duplicate values:
    positions = positions.filter(function (value, index, self) {
        return self.indexOf(value) === index;
    });

    for (let i = 0; i < positions.length; i++) {
        const position = positions[i];
        const positionAbs = Math.abs(position);
        const bitBytePosition = HatByteProcessor.getByteNoAndBitPos(positionAbs);
        // Check if position is not out of bound of the data view:
        if (bitBytePosition.byteNo < dataViewBounds) {
            let byte = self.dataView.getUint8(bitBytePosition.byteNo);
            byte = HatByteProcessor.changeBit(byte, bitBytePosition.bitPos, (position >= 0));
            self.dataView.setUint8(bitBytePosition.byteNo, byte);
        }
    }
    return self;
};

/**
 * Convert the whole ArrayBuffer to a string
 * (Hint: Could be used to store a hat in persistent storage as a encoded string)
 *
 * @returns {string} - Encoded string
 * @override
 */
Hat.prototype.toString = function () {
    return String.fromCharCode.apply(null, new Uint16Array(this.arrayBuffer));
};

/**
 * Class method to create a new Hat from a string
 * (Could be used to retrieve from persistent storage)
 * @param {string} str - Representing a new Hat instance
 * @returns {Hat} - new instance of a Hat
 * @throws {TypeError} if given argument is not an instance of string
 */
Hat.fromString = function (str = '') {
    if (typeof str !== 'string') {
        throw new TypeError('argument not a string');
    }
    const arrayBuffer = new ArrayBuffer(str.length * 2); // 2 bytes for each char
    const typedArray = new Uint16Array(arrayBuffer);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
        typedArray[i] = str.charCodeAt(i);
    }
    return new Hat(arrayBuffer);
};


/**
 * Creates a new empty Hat from a given byte size
 * @param {Number} byteSize - Allocate this Hat with a unsigned integer value (size in bytes)
 * @returns {Hat} - new instance of a Hat
 */
Hat.create = function (byteSize) {
    const arrayBuffer = new ArrayBuffer(byteSize);
    return new Hat(arrayBuffer);
};


module.exports = Hat;