/* eslint-disable no-unused-vars */
/* eslint-disable no-undef */
// @ts-nocheck
import { browser } from '$app/environment';
// 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/

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

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 =
  browser ?? 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;
}

export 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 = globalThis[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 oldGlobalThis = globalThis.jDataView;
  (globalThis.jDataView = jDataView).noConflict = function () {
    globalThis.jDataView = oldGlobalThis;
    return this;
  };
}
