//
// jDataView by Vjeux <vjeuxx@gmail.com> - Jan 2010
// Continued by RReverser <me@rreverser.com> - Feb 2013
//
// A unique way to work with a binary file in the browser
// http://github.com/jDataView/jDataView
// http://jDataView.github.io/

goog.provide('owg.DataView');

//var global = function(){return this;}
//(function (global) {

//'use strict';

var compatibility = {
    // NodeJS Buffer in v0.5.5 and newer
    //NodeBuffer: 'Buffer' in global && 'readInt16LE' in Buffer.prototype,
    /*DataView: 'DataView' in global && (
     'getFloat64' in DataView.prototype ||            // Chrome
     'getFloat64' in new DataView(new ArrayBuffer(1)) // Node
     ),
     ArrayBuffer: 'ArrayBuffer' in global,
     PixelData: 'CanvasPixelArray' in global && 'ImageData' in global && 'document' in global
     */
    DataView: true,
    ArrayBuffer: false,
    PixelData: false
};

// we don't want to bother with old Buffer implementation
/*if (compatibility.NodeBuffer) {
 (function (buffer) {
 try {
 buffer.writeFloatLE(Infinity, 0);
 } catch (e) {
 compatibility.NodeBuffer = false;
 }
 })(new Buffer(4));
 }*/

if (compatibility.PixelData) {
    var createPixelData = function (byteLength, buffer) {
        var data = createPixelData.context2d.createImageData((byteLength + 3) / 4, 1).data;
        data.byteLength = byteLength;
        if (buffer !== undefined) {
            for (var i = 0; i < byteLength; i++) {
                data[i] = buffer[i];
            }
        }
        return data;
    };
    createPixelData.context2d = document.createElement('canvas').getContext('2d');
}

var dataTypes = {
    'Int8': 1,
    'Int16': 2,
    'Int32': 4,
    'Uint8': 1,
    'Uint16': 2,
    'Uint32': 4,
    'Float32': 4,
    'Float64': 8
};

var nodeNaming = {
    'Int8': 'Int8',
    'Int16': 'Int16',
    'Int32': 'Int32',
    'Uint8': 'UInt8',
    'Uint16': 'UInt16',
    'Uint32': 'UInt32',
    'Float32': 'Float',
    'Float64': 'Double'
};

function arrayFrom(arrayLike, forceCopy) {
    return (!forceCopy && (arrayLike instanceof Array)) ? arrayLike : Array.prototype.slice.call(arrayLike);
}

function defined(value, defaultValue) {
    return value !== undefined ? value : defaultValue;
}

function jDataView(buffer, byteOffset, byteLength, littleEndian) {
    /* jshint validthis:true */

    if (buffer instanceof jDataView) {
        var result = buffer.slice(byteOffset, byteOffset + byteLength);
        result._littleEndian = defined(littleEndian, result._littleEndian);
        return result;
    }

    if (!(this instanceof jDataView)) {
        return new jDataView(buffer, byteOffset, byteLength, littleEndian);
    }

    this.buffer = buffer = jDataView.wrapBuffer(buffer);

    // Check parameters and existing functionnalities
    this._isArrayBuffer = compatibility.ArrayBuffer && buffer instanceof ArrayBuffer;
    this._isPixelData = compatibility.PixelData && buffer instanceof CanvasPixelArray;
    this._isDataView = compatibility.DataView && this._isArrayBuffer;
    //this._isNodeBuffer = compatibility.NodeBuffer && buffer instanceof Buffer;

    // Handle Type Errors
    if (/*!this._isNodeBuffer &&*/ !this._isArrayBuffer && !this._isPixelData && !(buffer instanceof Array)) {
        throw new TypeError('jDataView buffer has an incompatible type');
    }

    // Default Values
    this._littleEndian = !!littleEndian;

    var bufferLength = 'byteLength' in buffer ? buffer.byteLength : buffer.length;
    this.byteOffset = byteOffset = defined(byteOffset, 0);
    this.byteLength = byteLength = defined(byteLength, bufferLength - byteOffset);

    if (!this._isDataView) {
        this._checkBounds(byteOffset, byteLength, bufferLength);
    } else {
        this._view = new DataView(buffer, byteOffset, byteLength);
    }

    // Create uniform methods (action wrappers) for the following data types

    this._engineAction =
        this._isDataView
            ? this._dataViewAction
            /*                : this._isNodeBuffer
             ? this._nodeBufferAction*/
            : this._isArrayBuffer
            ? this._arrayBufferAction
            : this._arrayAction;
}

function getCharCodes(string) {
    /*if (compatibility.NodeBuffer) {
     return new Buffer(string, 'binary');
     }*/

    var Type = compatibility.ArrayBuffer ? Uint8Array : Array,
        codes = new Type(string.length);

    for (var i = 0, length = string.length; i < length; i++) {
        codes[i] = string.charCodeAt(i) & 0xff;
    }
    return codes;
}

// mostly internal function for wrapping any supported input (String or Array-like) to best suitable buffer format
jDataView.wrapBuffer = function (buffer) {
    switch (typeof buffer) {
        case 'number':
            /*if (compatibility.NodeBuffer) {
             buffer = new Buffer(buffer);
             buffer.fill(0);
             } else*/
            if (compatibility.ArrayBuffer) {
                buffer = new Uint8Array(buffer).buffer;
            } else
            if (compatibility.PixelData) {
                buffer = createPixelData(buffer);
            } else {
                buffer = new Array(buffer);
                for (var i = 0; i < buffer.length; i++) {
                    buffer[i] = 0;
                }
            }
            return buffer;

        case 'string':
            buffer = getCharCodes(buffer);
        /* falls through */
        default:
            //if ('length' in buffer && /*!((compatibility.NodeBuffer && buffer instanceof Buffer) ||*/( (compatibility.ArrayBuffer && buffer instanceof ArrayBuffer) || (compatibility.PixelData && buffer instanceof CanvasPixelArray))) {
            /*if (compatibility.NodeBuffer) {
             buffer = new Buffer(buffer);
             } else*/
            if (compatibility.ArrayBuffer) {
                if (!(buffer instanceof ArrayBuffer)) {
                    buffer = new Uint8Array(buffer).buffer;
                    // bug in Node.js <= 0.8:
                    if (!(buffer instanceof ArrayBuffer)) {
                        buffer = new Uint8Array(arrayFrom(buffer, true)).buffer;
                    }
                }
            } else
            if (compatibility.PixelData) {
                buffer = createPixelData(buffer.length, buffer);
            } else {
                buffer = arrayFrom(buffer);
            }
            //}
            return buffer;
    }
};

function pow2(n) {
    return (n >= 0 && n < 31) ? (1 << n) : (pow2[n] || (pow2[n] = Math.pow(2, n)));
}

// left for backward compatibility
jDataView.createBuffer = function () {
    return jDataView.wrapBuffer(arguments);
};

function Uint64(lo, hi) {
    this.lo = lo;
    this.hi = hi;
}

jDataView.Uint64 = Uint64;

Uint64.prototype = {
    valueOf: function () {
        return this.lo + pow2(32) * this.hi;
    },

    toString: function () {
        return Number.prototype.toString.apply(this.valueOf(), arguments);
    }
};

Uint64.fromNumber = function (number) {
    var hi = Math.floor(number / pow2(32)),
        lo = number - hi * pow2(32);

    return new Uint64(lo, hi);
};

function Int64(lo, hi) {
    Uint64.apply(this, arguments);
}

jDataView.Int64 = Int64;

Int64.prototype = 'create' in Object ? Object.create(Uint64.prototype) : new Uint64();

Int64.prototype.valueOf = function () {
    if (this.hi < pow2(31)) {
        return Uint64.prototype.valueOf.apply(this, arguments);
    }
    return -((pow2(32) - this.lo) + pow2(32) * (pow2(32) - 1 - this.hi));
};

Int64.fromNumber = function (number) {
    var lo, hi;
    if (number >= 0) {
        var unsigned = Uint64.fromNumber(number);
        lo = unsigned.lo;
        hi = unsigned.hi;
    } else {
        hi = Math.floor(number / pow2(32));
        lo = number - hi * pow2(32);
        hi += pow2(32);
    }
    return new Int64(lo, hi);
};

jDataView.prototype = {
    _offset: 0,
    _bitOffset: 0,

    compatibility: compatibility,

    _checkBounds: function (byteOffset, byteLength, maxLength) {
        // Do additional checks to simulate DataView
        if (typeof byteOffset !== 'number') {
            throw new TypeError('Offset is not a number.');
        }
        if (typeof byteLength !== 'number') {
            throw new TypeError('Size is not a number.');
        }
        if (byteLength < 0) {
            throw new RangeError('Length is negative.');
        }
        if (byteOffset < 0 || byteOffset + byteLength > defined(maxLength, this.byteLength)) {
            throw new RangeError('Offsets are out of bounds.');
        }
    },

    _action: function (type, isReadAction, byteOffset, littleEndian, value) {
        return this._engineAction(
            type,
            isReadAction,
            defined(byteOffset, this._offset),
            defined(littleEndian, this._littleEndian),
            value
        );
    },

    _dataViewAction: function (type, isReadAction, byteOffset, littleEndian, value) {
        // Move the internal offset forward
        this._offset = byteOffset + dataTypes[type];
        return isReadAction ? this._view['get' + type](byteOffset, littleEndian) : this._view['set' + type](byteOffset, value, littleEndian);
    },

    _nodeBufferAction: function (type, isReadAction, byteOffset, littleEndian, value) {
        // Move the internal offset forward
        this._offset = byteOffset + dataTypes[type];
        var nodeName = nodeNaming[type] + ((type === 'Int8' || type === 'Uint8') ? '' : littleEndian ? 'LE' : 'BE');
        byteOffset += this.byteOffset;
        return isReadAction ? this.buffer['read' + nodeName](byteOffset) : this.buffer['write' + nodeName](value, byteOffset);
    },

    _arrayBufferAction: function (type, isReadAction, byteOffset, littleEndian, value) {
        /*var size = dataTypes[type], TypedArray = global[type + 'Array'], typedArray;

        littleEndian = defined(littleEndian, this._littleEndian);

        // ArrayBuffer: we use a typed array of size 1 from original buffer if alignment is good and from slice when it's not
        if (size === 1 || ((this.byteOffset + byteOffset) % size === 0 && littleEndian)) {
            typedArray = new TypedArray(this.buffer, this.byteOffset + byteOffset, 1);
            this._offset = byteOffset + size;
            return isReadAction ? typedArray[0] : (typedArray[0] = value);
        } else {
            var bytes = new Uint8Array(isReadAction ? this.getBytes(size, byteOffset, littleEndian, true) : size);
            typedArray = new TypedArray(bytes.buffer, 0, 1);

            if (isReadAction) {
                return typedArray[0];
            } else {
                typedArray[0] = value;
                this._setBytes(byteOffset, bytes, littleEndian);
            }
        }*/
    },

    _arrayAction: function (type, isReadAction, byteOffset, littleEndian, value) {
        return isReadAction ? this['_get' + type](byteOffset, littleEndian) : this['_set' + type](byteOffset, value, littleEndian);
    },

    // Helpers

    _getBytes: function (length, byteOffset, littleEndian) {
        littleEndian = defined(littleEndian, this._littleEndian);
        byteOffset = defined(byteOffset, this._offset);
        length = defined(length, this.byteLength - byteOffset);

        this._checkBounds(byteOffset, length);

        byteOffset += this.byteOffset;

        this._offset = byteOffset - this.byteOffset + length;

        var result = this._isArrayBuffer
            ? new Uint8Array(this.buffer, byteOffset, length)
            : (this.buffer.slice || Array.prototype.slice).call(this.buffer, byteOffset, byteOffset + length);

        return littleEndian || length <= 1 ? result : arrayFrom(result).reverse();
    },

    // wrapper for external calls (do not return inner buffer directly to prevent it's modifying)
    getBytes: function (length, byteOffset, littleEndian, toArray) {
        var result = this._getBytes(length, byteOffset, defined(littleEndian, true));
        return toArray ? arrayFrom(result) : result;
    },

    _setBytes: function (byteOffset, bytes, littleEndian) {
        var length = bytes.length;

        // needed for Opera
        if (length === 0) {
            return;
        }

        littleEndian = defined(littleEndian, this._littleEndian);
        byteOffset = defined(byteOffset, this._offset);

        this._checkBounds(byteOffset, length);

        if (!littleEndian && length > 1) {
            bytes = arrayFrom(bytes, true).reverse();
        }

        byteOffset += this.byteOffset;

        if (this._isArrayBuffer) {
            new Uint8Array(this.buffer, byteOffset, length).set(bytes);
        }
        else {
            /*if (this._isNodeBuffer) {
             new Buffer(bytes).copy(this.buffer, byteOffset);
             } else*/ {
                for (var i = 0; i < length; i++) {
                    this.buffer[byteOffset + i] = bytes[i];
                }
            }
        }

        this._offset = byteOffset - this.byteOffset + length;
    },

    setBytes: function (byteOffset, bytes, littleEndian) {
        this._setBytes(byteOffset, bytes, defined(littleEndian, true));
    },

    getString: function (byteLength, byteOffset, encoding) {
        if (this._isNodeBuffer) {
            byteOffset = defined(byteOffset, this._offset);
            byteLength = defined(byteLength, this.byteLength - byteOffset);

            this._checkBounds(byteOffset, byteLength);

            this._offset = byteOffset + byteLength;
            return this.buffer.toString(encoding || 'binary', this.byteOffset + byteOffset, this.byteOffset + this._offset);
        }
        var bytes = this._getBytes(byteLength, byteOffset, true), string = '';
        byteLength = bytes.length;
        for (var i = 0; i < byteLength; i++) {
            string += String.fromCharCode(bytes[i]);
        }
        if (encoding === 'utf8') {
            string = decodeURIComponent(escape(string));
        }
        return string;
    },

    setString: function (byteOffset, subString, encoding) {
        if (this._isNodeBuffer) {
            byteOffset = defined(byteOffset, this._offset);
            this._checkBounds(byteOffset, subString.length);
            this._offset = byteOffset + this.buffer.write(subString, this.byteOffset + byteOffset, encoding || 'binary');
            return;
        }
        if (encoding === 'utf8') {
            subString = unescape(encodeURIComponent(subString));
        }
        this._setBytes(byteOffset, getCharCodes(subString), true);
    },

    getChar: function (byteOffset) {
        return this.getString(1, byteOffset);
    },

    setChar: function (byteOffset, character) {
        this.setString(byteOffset, character);
    },

    tell: function () {
        return this._offset;
    },

    seek: function (byteOffset) {
        this._checkBounds(byteOffset, 0);
        /* jshint boss: true */
        return this._offset = byteOffset;
    },

    skip: function (byteLength) {
        return this.seek(this._offset + byteLength);
    },

    slice: function (start, end, forceCopy) {
        function normalizeOffset(offset, byteLength) {
            return offset < 0 ? offset + byteLength : offset;
        }

        start = normalizeOffset(start, this.byteLength);
        end = normalizeOffset(defined(end, this.byteLength), this.byteLength);

        return forceCopy
            ? new jDataView(this.getBytes(end - start, start, true, true), undefined, undefined, this._littleEndian)
            : new jDataView(this.buffer, this.byteOffset + start, end - start, this._littleEndian);
    },

    alignBy: function (byteCount) {
        this._bitOffset = 0;
        if (defined(byteCount, 1) !== 1) {
            return this.skip(byteCount - (this._offset % byteCount || byteCount));
        } else {
            return this._offset;
        }
    },

    // Compatibility functions

    _getFloat64: function (byteOffset, littleEndian) {
        var b = this._getBytes(8, byteOffset, littleEndian),

            sign = 1 - (2 * (b[7] >> 7)),
            exponent = ((((b[7] << 1) & 0xff) << 3) | (b[6] >> 4)) - ((1 << 10) - 1),

        // Binary operators such as | and << operate on 32 bit values, using + and Math.pow(2) instead
            mantissa = ((b[6] & 0x0f) * pow2(48)) + (b[5] * pow2(40)) + (b[4] * pow2(32)) +
                (b[3] * pow2(24)) + (b[2] * pow2(16)) + (b[1] * pow2(8)) + b[0];

        if (exponent === 1024) {
            if (mantissa !== 0) {
                return NaN;
            } else {
                return sign * Infinity;
            }
        }

        if (exponent === -1023) { // Denormalized
            return sign * mantissa * pow2(-1022 - 52);
        }

        return sign * (1 + mantissa * pow2(-52)) * pow2(exponent);
    },

    _getFloat32: function (byteOffset, littleEndian) {
        var b = this._getBytes(4, byteOffset, littleEndian),

            sign = 1 - (2 * (b[3] >> 7)),
            exponent = (((b[3] << 1) & 0xff) | (b[2] >> 7)) - 127,
            mantissa = ((b[2] & 0x7f) << 16) | (b[1] << 8) | b[0];

        if (exponent === 128) {
            if (mantissa !== 0) {
                return NaN;
            } else {
                return sign * Infinity;
            }
        }

        if (exponent === -127) { // Denormalized
            return sign * mantissa * pow2(-126 - 23);
        }

        return sign * (1 + mantissa * pow2(-23)) * pow2(exponent);
    },

    _get64: function (Type, byteOffset, littleEndian) {
        littleEndian = defined(littleEndian, this._littleEndian);
        byteOffset = defined(byteOffset, this._offset);

        var parts = littleEndian ? [0, 4] : [4, 0];

        for (var i = 0; i < 2; i++) {
            parts[i] = this.getUint32(byteOffset + parts[i], littleEndian);
        }

        this._offset = byteOffset + 8;

        return new Type(parts[0], parts[1]);
    },

    getInt64: function (byteOffset, littleEndian) {
        return this._get64(Int64, byteOffset, littleEndian);
    },

    getUint64: function (byteOffset, littleEndian) {
        return this._get64(Uint64, byteOffset, littleEndian);
    },

    _getInt32: function (byteOffset, littleEndian) {
        var b = this._getBytes(4, byteOffset, littleEndian);
        return (b[3] << 24) | (b[2] << 16) | (b[1] << 8) | b[0];
    },

    _getUint32: function (byteOffset, littleEndian) {
        return this._getInt32(byteOffset, littleEndian) >>> 0;
    },

    _getInt16: function (byteOffset, littleEndian) {
        return (this._getUint16(byteOffset, littleEndian) << 16) >> 16;
    },

    _getUint16: function (byteOffset, littleEndian) {
        var b = this._getBytes(2, byteOffset, littleEndian);
        return (b[1] << 8) | b[0];
    },

    _getInt8: function (byteOffset) {
        return (this._getUint8(byteOffset) << 24) >> 24;
    },

    _getUint8: function (byteOffset) {
        return this._getBytes(1, byteOffset)[0];
    },

    _getBitRangeData: function (bitLength, byteOffset) {
        var startBit = (defined(byteOffset, this._offset) << 3) + this._bitOffset,
            endBit = startBit + bitLength,
            start = startBit >>> 3,
            end = (endBit + 7) >>> 3,
            b = this._getBytes(end - start, start, true),
            wideValue = 0;

        /* jshint boss: true */
        if (this._bitOffset = endBit & 7) {
            this._bitOffset -= 8;
        }

        for (var i = 0, length = b.length; i < length; i++) {
            wideValue = (wideValue << 8) | b[i];
        }

        return {
            start: start,
            bytes: b,
            wideValue: wideValue
        };
    },

    getSigned: function (bitLength, byteOffset) {
        var shift = 32 - bitLength;
        return (this.getUnsigned(bitLength, byteOffset) << shift) >> shift;
    },

    getUnsigned: function (bitLength, byteOffset) {
        var value = this._getBitRangeData(bitLength, byteOffset).wideValue >>> -this._bitOffset;
        return bitLength < 32 ? (value & ~(-1 << bitLength)) : value;
    },

    _setBinaryFloat: function (byteOffset, value, mantSize, expSize, littleEndian) {
        var signBit = value < 0 ? 1 : 0,
            exponent,
            mantissa,
            eMax = ~(-1 << (expSize - 1)),
            eMin = 1 - eMax;

        if (value < 0) {
            value = -value;
        }

        if (value === 0) {
            exponent = 0;
            mantissa = 0;
        } else if (isNaN(value)) {
            exponent = 2 * eMax + 1;
            mantissa = 1;
        } else if (value === Infinity) {
            exponent = 2 * eMax + 1;
            mantissa = 0;
        } else {
            exponent = Math.floor(Math.log(value) / Math.LN2);
            if (exponent >= eMin && exponent <= eMax) {
                mantissa = Math.floor((value * pow2(-exponent) - 1) * pow2(mantSize));
                exponent += eMax;
            } else {
                mantissa = Math.floor(value / pow2(eMin - mantSize));
                exponent = 0;
            }
        }

        var b = [];
        while (mantSize >= 8) {
            b.push(mantissa % 256);
            mantissa = Math.floor(mantissa / 256);
            mantSize -= 8;
        }
        exponent = (exponent << mantSize) | mantissa;
        expSize += mantSize;
        while (expSize >= 8) {
            b.push(exponent & 0xff);
            exponent >>>= 8;
            expSize -= 8;
        }
        b.push((signBit << expSize) | exponent);

        this._setBytes(byteOffset, b, littleEndian);
    },

    _setFloat32: function (byteOffset, value, littleEndian) {
        this._setBinaryFloat(byteOffset, value, 23, 8, littleEndian);
    },

    _setFloat64: function (byteOffset, value, littleEndian) {
        this._setBinaryFloat(byteOffset, value, 52, 11, littleEndian);
    },

    _set64: function (Type, byteOffset, value, littleEndian) {
        if (!(value instanceof Type)) {
            value = Type.fromNumber(value);
        }

        littleEndian = defined(littleEndian, this._littleEndian);
        byteOffset = defined(byteOffset, this._offset);

        var parts = littleEndian ? {lo: 0, hi: 4} : {lo: 4, hi: 0};

        for (var partName in parts) {
            this.setUint32(byteOffset + parts[partName], value[partName], littleEndian);
        }

        this._offset = byteOffset + 8;
    },

    setInt64: function (byteOffset, value, littleEndian) {
        this._set64(Int64, byteOffset, value, littleEndian);
    },

    setUint64: function (byteOffset, value, littleEndian) {
        this._set64(Uint64, byteOffset, value, littleEndian);
    },

    _setUint32: function (byteOffset, value, littleEndian) {
        this._setBytes(byteOffset, [
            value & 0xff,
            (value >>> 8) & 0xff,
            (value >>> 16) & 0xff,
            value >>> 24
        ], littleEndian);
    },

    _setUint16: function (byteOffset, value, littleEndian) {
        this._setBytes(byteOffset, [
            value & 0xff,
            (value >>> 8) & 0xff
        ], littleEndian);
    },

    _setUint8: function (byteOffset, value) {
        this._setBytes(byteOffset, [value & 0xff]);
    },

    setUnsigned: function (byteOffset, value, bitLength) {
        var data = this._getBitRangeData(bitLength, byteOffset),
            wideValue = data.wideValue,
            b = data.bytes;

        wideValue &= ~(~(-1 << bitLength) << -this._bitOffset); // clearing bit range before binary "or"
        wideValue |= (bitLength < 32 ? (value & ~(-1 << bitLength)) : value) << -this._bitOffset; // setting bits

        for (var i = b.length - 1; i >= 0; i--) {
            b[i] = wideValue & 0xff;
            wideValue >>>= 8;
        }

        this._setBytes(data.start, b, true);
    }
};

var proto = jDataView.prototype;

for (var type in dataTypes) {
    (function (type) {
        proto['get' + type] = function (byteOffset, littleEndian) {
            return this._action(type, true, byteOffset, littleEndian);
        };
        proto['set' + type] = function (byteOffset, value, littleEndian) {
            this._action(type, false, byteOffset, littleEndian, value);
        };
    })(type);
}

proto._setInt32 = proto._setUint32;
proto._setInt16 = proto._setUint16;
proto._setInt8 = proto._setUint8;
proto.setSigned = proto.setUnsigned;

for (var method in proto) {
    if (method.slice(0, 3) === 'set') {
        (function (type) {
            proto['write' + type] = function () {
                Array.prototype.unshift.call(arguments, undefined);
                this['set' + type].apply(this, arguments);
            };
        })(method.slice(3));
    }
}

/*if (typeof module !== 'undefined' && typeof module.exports === 'object') {
 module.exports = jDataView;
 } else
 if (typeof define === 'function' && define.amd) {
 define([], function () { return jDataView });
 } else {
 var oldGlobal = global.jDataView;
 (global.jDataView = jDataView).noConflict = function () {
 global.jDataView = oldGlobal;
 return this;
 };
 }*/

//})((function () { /* jshint strict: false */ return this })());