export class CustomType {
  withFields(fields) {
    let properties = Object.keys(this).map((label) =>
      label in fields ? fields[label] : this[label],
    );
    return new this.constructor(...properties);
  }
}

export class List {
  static fromArray(array, tail) {
    return toList(array, tail)
  }

  [Symbol.iterator]() {
    return new ListIterator(this);
  }

  toArray() {
    return [...this];
  }

  atLeastLength(desired) {
    let current = this;
    while (desired-- > 0 && current) current = current.tail;
    return current !== undefined;
  }

  hasLength(desired) {
    let current = this;
    while (desired-- > 0 && current) current = current.tail;
    return desired === -1 && current instanceof Empty;
  }

  countLength() {
    let current = this;
    let length = 0;
    while (current) {
      current = current.tail;
      length++;
    }
    return length - 1;
  }
}

export function prepend(element, tail) {
  return new NonEmpty(element, tail);
}

export function toList(elements, tail) {
  let t = tail || List$Empty$const
  for (let i = elements.length - 1; i >= 0; --i) {
    t = new NonEmpty(elements[i], t);
  }
  return t;
}

class ListIterator {
  #current;

  constructor(current) {
    this.#current = current;
  }

  next() {
    if (this.#current instanceof Empty) {
      return { done: true };
    } else {
      let { head, tail } = this.#current;
      this.#current = tail;
      return { value: head, done: false };
    }
  }
}

export class Empty extends List { }
export const List$Empty$const = new Empty();
export const List$Empty = () => List$Empty$const;
export const List$isEmpty = (value) => value instanceof Empty;

export class NonEmpty extends List {
  constructor(head, tail) {
    super();
    this.head = head;
    this.tail = tail;
  }
}
export const List$NonEmpty = (head, tail) => new NonEmpty(head, tail);
export const List$isNonEmpty = (value) => value instanceof NonEmpty;

export const List$NonEmpty$first = (value) => value.head;
export const List$NonEmpty$rest = (value) => value.tail;

/**
 * A bit array is a contiguous sequence of bits similar to Erlang's Binary type.
 */
export class BitArray {
  /**
   * The size in bits of this bit array's data.
   *
   * @type {number}
   */
  bitSize;

  /**
   * The size in bytes of this bit array's data. If this bit array doesn't store
   * a whole number of bytes then this value is rounded up.
   *
   * @type {number}
   */
  byteSize;

  /**
   * The number of unused high bits in the first byte of this bit array's
   * buffer prior to the start of its data. The value of any unused high bits is
   * undefined.
   *
   * The bit offset will be in the range 0-7.
   *
   * @type {number}
   */
  bitOffset;

  /**
   * The raw bytes that hold this bit array's data.
   *
   * If `bitOffset` is not zero then there are unused high bits in the first
   * byte of this buffer.
   *
   * If `bitOffset + bitSize` is not a multiple of 8 then there are unused low
   * bits in the last byte of this buffer.
   *
   * @type {Uint8Array}
   */
  rawBuffer;

  /**
   * Constructs a new bit array from a `Uint8Array`, an optional size in
   * bits, and an optional bit offset.
   *
   * If no bit size is specified it is taken as `buffer.length * 8`, i.e. all
   * bytes in the buffer make up the new bit array's data.
   *
   * If no bit offset is specified it defaults to zero, i.e. there are no unused
   * high bits in the first byte of the buffer.
   *
   * @param {Uint8Array} buffer
   * @param {number} [bitSize]
   * @param {number} [bitOffset]
   */
  constructor(buffer, bitSize, bitOffset) {
    if (!(buffer instanceof Uint8Array)) {
      throw globalThis.Error(
        "BitArray can only be constructed from a Uint8Array",
      );
    }

    this.bitSize = bitSize ?? buffer.length * 8;
    this.byteSize = Math.trunc((this.bitSize + 7) / 8);
    this.bitOffset = bitOffset ?? 0;

    // Validate the bit size
    if (this.bitSize < 0) {
      throw globalThis.Error(`BitArray bit size is invalid: ${this.bitSize}`);
    }

    // Validate the bit offset
    if (this.bitOffset < 0 || this.bitOffset > 7) {
      throw globalThis.Error(
        `BitArray bit offset is invalid: ${this.bitOffset}`,
      );
    }

    // Validate the length of the buffer
    if (buffer.length !== Math.trunc((this.bitOffset + this.bitSize + 7) / 8)) {
      throw globalThis.Error("BitArray buffer length is invalid");
    }

    this.rawBuffer = buffer;
  }

  /**
   * Returns a specific byte in this bit array. If the byte index is out of
   * range then `undefined` is returned.
   *
   * When returning the final byte of a bit array with a bit size that's not a
   * multiple of 8, the content of the unused low bits are undefined.
   *
   * @param {number} index
   * @returns {number | undefined}
   */
  byteAt(index) {
    if (index < 0 || index >= this.byteSize) {
      return undefined;
    }

    return bitArrayByteAt(this.rawBuffer, this.bitOffset, index);
  }

  equals(other) {
    if (this.bitSize !== other.bitSize) {
      return false;
    }

    const wholeByteCount = Math.trunc(this.bitSize / 8);

    // If both bit offsets are zero do a byte-aligned equality check which is
    // faster
    if (this.bitOffset === 0 && other.bitOffset === 0) {
      // Compare any whole bytes
      for (let i = 0; i < wholeByteCount; i++) {
        if (this.rawBuffer[i] !== other.rawBuffer[i]) {
          return false;
        }
      }

      // Compare any trailing bits, excluding unused low bits
      const trailingBitsCount = this.bitSize % 8;
      if (trailingBitsCount) {
        const unusedLowBitCount = 8 - trailingBitsCount;
        if (
          this.rawBuffer[wholeByteCount] >> unusedLowBitCount !==
          other.rawBuffer[wholeByteCount] >> unusedLowBitCount
        ) {
          return false;
        }
      }
    } else {
      // Compare any whole bytes
      for (let i = 0; i < wholeByteCount; i++) {
        const a = bitArrayByteAt(this.rawBuffer, this.bitOffset, i);
        const b = bitArrayByteAt(other.rawBuffer, other.bitOffset, i);

        if (a !== b) {
          return false;
        }
      }

      // Compare any trailing bits
      const trailingBitsCount = this.bitSize % 8;
      if (trailingBitsCount) {
        const a = bitArrayByteAt(
          this.rawBuffer,
          this.bitOffset,
          wholeByteCount,
        );
        const b = bitArrayByteAt(
          other.rawBuffer,
          other.bitOffset,
          wholeByteCount,
        );

        const unusedLowBitCount = 8 - trailingBitsCount;
        if (a >> unusedLowBitCount !== b >> unusedLowBitCount) {
          return false;
        }
      }
    }

    return true;
  }

  /**
   * Returns this bit array's internal buffer.
   *
   * @deprecated
   *
   * @returns {Uint8Array}
   */
  get buffer() {
    if (this.bitOffset !== 0 || this.bitSize % 8 !== 0) {
      throw new globalThis.Error(
        "BitArray.buffer does not support unaligned bit arrays",
      );
    }

    return this.rawBuffer;
  }

  /**
   * Returns the length in bytes of this bit array's internal buffer.
   *
   * @deprecated
   *
   * @returns {number}
   */
  get length() {
    if (this.bitOffset !== 0 || this.bitSize % 8 !== 0) {
      throw new globalThis.Error(
        "BitArray.length does not support unaligned bit arrays",
      );
    }

    return this.rawBuffer.length;
  }
}

export const BitArray$BitArray = (buffer, bitSize, bitOffset) =>
  new BitArray(buffer, bitSize, bitOffset);
export const BitArray$isBitArray = (value) => value instanceof BitArray;
export const BitArray$BitArray$data = (bitArray) => {
  if (bitArray.bitSize % 8 !== 0)
    throw new globalThis.Error(
      "BitArray$BitArray$data called on un-aligned bit array",
    );
  const array = bitArray.rawBuffer;
  return new DataView(array.buffer, array.byteOffset, bitArray.byteSize);
};

/**
 * Returns the nth byte in the given buffer, after applying the specified bit
 * offset. If the index is out of bounds then zero is returned.
 *
 * @param {Uint8Array} buffer
 * @param {number} bitOffset
 * @param {number} index
 * @returns {number}
 */
function bitArrayByteAt(buffer, bitOffset, index) {
  if (bitOffset === 0) {
    return buffer[index] ?? 0;
  } else {
    const a = (buffer[index] << bitOffset) & 0xff;
    const b = buffer[index + 1] >> (8 - bitOffset);

    return a | b;
  }
}

export class UtfCodepoint {
  constructor(value) {
    this.value = value;
  }
}

/**
 * Slices a bit array to produce a new bit array. If `end` is not supplied then
 * all bits from `start` onward are returned.
 *
 * If the slice is out of bounds then an exception is thrown.
 *
 * @param {BitArray} bitArray
 * @param {number} start
 * @param {number} [end]
 * @returns {BitArray}
 */
export function bitArraySlice(bitArray, start, end) {
  end ??= bitArray.bitSize;

  bitArrayValidateRange(bitArray, start, end);

  // Handle zero-length slices
  if (start === end) {
    return new BitArray(new Uint8Array());
  }

  // Early return for slices that cover the whole bit array
  if (start === 0 && end === bitArray.bitSize) {
    return bitArray;
  }

  start += bitArray.bitOffset;
  end += bitArray.bitOffset;

  const startByteIndex = Math.trunc(start / 8);
  const endByteIndex = Math.trunc((end + 7) / 8);
  const byteLength = endByteIndex - startByteIndex;

  // Avoid creating a new Uint8Array if the view of the underlying ArrayBuffer
  // is the same. This can occur when slicing off just the first or last bit of
  // a bit array, i.e. when only the bit offset or bit size need to be updated.
  let buffer;
  if (startByteIndex === 0 && byteLength === bitArray.rawBuffer.byteLength) {
    buffer = bitArray.rawBuffer;
  } else {
    buffer = new Uint8Array(
      bitArray.rawBuffer.buffer,
      bitArray.rawBuffer.byteOffset + startByteIndex,
      byteLength,
    );
  }

  return new BitArray(buffer, end - start, start % 8);
}

/**
 * Interprets a slice of this bit array as a floating point number, either
 * 32-bit or 64-bit, with the specified endianness.
 *
 * The value of `end - start` must be exactly 32 or 64, otherwise an exception
 * will be thrown.
 *
 * @param {BitArray} bitArray
 * @param {number} start
 * @param {number} end
 * @param {boolean} isBigEndian
 * @returns {number}
 */
export function bitArraySliceToFloat(bitArray, start, end, isBigEndian) {
  bitArrayValidateRange(bitArray, start, end);

  const floatSize = end - start;

  // Check size is valid
  if (floatSize !== 16 && floatSize !== 32 && floatSize !== 64) {
    const msg =
      `Sized floats must be 16-bit, 32-bit or 64-bit, got size of ` +
      `${floatSize} bits`;
    throw new globalThis.Error(msg);
  }

  start += bitArray.bitOffset;

  const isStartByteAligned = start % 8 === 0;

  // If the bit range is byte aligned then the float can be read directly out
  // of the existing buffer
  if (isStartByteAligned) {
    const view = new DataView(
      bitArray.rawBuffer.buffer,
      bitArray.rawBuffer.byteOffset + start / 8,
    );

    if (floatSize === 64) {
      return view.getFloat64(0, !isBigEndian);
    } else if (floatSize === 32) {
      return view.getFloat32(0, !isBigEndian);
    } else if (floatSize === 16) {
      return fp16UintToNumber(view.getUint16(0, !isBigEndian));
    }
  }

  // Copy the unaligned bytes into an aligned array so a DataView can be used
  const alignedBytes = new Uint8Array(floatSize / 8);
  const byteOffset = Math.trunc(start / 8);
  for (let i = 0; i < alignedBytes.length; i++) {
    alignedBytes[i] = bitArrayByteAt(
      bitArray.rawBuffer,
      start % 8,
      byteOffset + i,
    );
  }

  // Read the float out of the aligned buffer
  const view = new DataView(alignedBytes.buffer);
  if (floatSize === 64) {
    return view.getFloat64(0, !isBigEndian);
  } else if (floatSize === 32) {
    return view.getFloat32(0, !isBigEndian);
  } else {
    return fp16UintToNumber(view.getUint16(0, !isBigEndian));
  }
}

/**
 * Interprets a slice of this bit array as a signed or unsigned integer with the
 * specified endianness.
 *
 * @param {BitArray} bitArray
 * @param {number} start
 * @param {number} end
 * @param {boolean} isBigEndian
 * @param {boolean} isSigned
 * @returns {number}
 */
export function bitArraySliceToInt(
  bitArray,
  start,
  end,
  isBigEndian,
  isSigned,
) {
  bitArrayValidateRange(bitArray, start, end);

  if (start === end) {
    return 0;
  }

  start += bitArray.bitOffset;
  end += bitArray.bitOffset;

  const isStartByteAligned = start % 8 === 0;
  const isEndByteAligned = end % 8 === 0;

  // If the slice is byte-aligned then there is no need to handle unaligned
  // slices, meaning a simpler and faster implementation can be used instead
  if (isStartByteAligned && isEndByteAligned) {
    return intFromAlignedSlice(
      bitArray,
      start / 8,
      end / 8,
      isBigEndian,
      isSigned,
    );
  }

  const size = end - start;

  const startByteIndex = Math.trunc(start / 8);
  const endByteIndex = Math.trunc((end - 1) / 8);

  // Handle the case of the slice being completely contained in a single byte
  if (startByteIndex == endByteIndex) {
    const mask = 0xff >> (start % 8);
    const unusedLowBitCount = (8 - (end % 8)) % 8;

    let value =
      (bitArray.rawBuffer[startByteIndex] & mask) >> unusedLowBitCount;

    // For signed integers, if the high bit is set reinterpret as two's
    // complement
    if (isSigned) {
      const highBit = 2 ** (size - 1);
      if (value >= highBit) {
        value -= highBit * 2;
      }
    }

    return value;
  }

  // The integer value to be read is not aligned and crosses at least one byte
  // boundary in the input array

  if (size <= 53) {
    return intFromUnalignedSliceUsingNumber(
      bitArray.rawBuffer,
      start,
      end,
      isBigEndian,
      isSigned,
    );
  } else {
    return intFromUnalignedSliceUsingBigInt(
      bitArray.rawBuffer,
      start,
      end,
      isBigEndian,
      isSigned,
    );
  }
}

/**
 * Joins the given segments into a new bit array, tightly packing them together.
 * Each segment must be one of the following types:
 *
 * - A `number`: A single byte value in the range 0-255. Values outside this
 *   range will be wrapped.
 * - A `Uint8Array`: A sequence of byte values of any length.
 * - A `BitArray`: A sequence of bits of any length, which may not be byte
 *   aligned.
 *
 * The bit size of the returned bit array will be the sum of the size in bits
 * of the input segments.
 *
 * @param {(number | Uint8Array | BitArray)[]} segments
 * @returns {BitArray}
 */
export function toBitArray(segments) {
  if (segments.length === 0) {
    return new BitArray(new Uint8Array());
  }

  if (segments.length === 1) {
    const segment = segments[0];

    // When there is a single BitArray segment it can be returned as-is
    if (segment instanceof BitArray) {
      return segment;
    }

    // When there is a single Uint8Array segment, pass it directly to the bit
    // array constructor to avoid a copy
    if (segment instanceof Uint8Array) {
      return new BitArray(segment);
    }

    return new BitArray(new Uint8Array(/** @type {number[]} */(segments)));
  }

  // Count the total number of bits and check if all segments are numbers, i.e.
  // single bytes
  let bitSize = 0;
  let areAllSegmentsNumbers = true;
  for (const segment of segments) {
    if (segment instanceof BitArray) {
      bitSize += segment.bitSize;
      areAllSegmentsNumbers = false;
    } else if (segment instanceof Uint8Array) {
      bitSize += segment.byteLength * 8;
      areAllSegmentsNumbers = false;
    } else {
      bitSize += 8;
    }
  }

  // If all segments are numbers then pass the segments array directly to the
  // Uint8Array constructor
  if (areAllSegmentsNumbers) {
    return new BitArray(new Uint8Array(/** @type {number[]} */(segments)));
  }

  // Pack the segments into a Uint8Array
  const buffer = new Uint8Array(Math.trunc((bitSize + 7) / 8));

  // The current write position in bits into the above array. Byte-aligned
  // segments, i.e. when the cursor is a multiple of 8, are able to be processed
  // faster due to being able to copy bytes directly.
  let cursor = 0;

  for (let segment of segments) {
    const isCursorByteAligned = cursor % 8 === 0;

    if (segment instanceof BitArray) {
      if (isCursorByteAligned && segment.bitOffset === 0) {
        buffer.set(segment.rawBuffer, cursor / 8);
        cursor += segment.bitSize;

        // Zero any unused bits in the last byte of the buffer. Their content is
        // undefined and shouldn't be included in the output.
        const trailingBitsCount = segment.bitSize % 8;
        if (trailingBitsCount !== 0) {
          const lastByteIndex = Math.trunc(cursor / 8);
          buffer[lastByteIndex] >>= 8 - trailingBitsCount;
          buffer[lastByteIndex] <<= 8 - trailingBitsCount;
        }
      } else {
        appendUnalignedBits(
          segment.rawBuffer,
          segment.bitSize,
          segment.bitOffset,
        );
      }
    } else if (segment instanceof Uint8Array) {
      if (isCursorByteAligned) {
        buffer.set(segment, cursor / 8);
        cursor += segment.byteLength * 8;
      } else {
        appendUnalignedBits(segment, segment.byteLength * 8, 0);
      }
    } else {
      if (isCursorByteAligned) {
        buffer[cursor / 8] = segment;
        cursor += 8;
      } else {
        appendUnalignedBits(new Uint8Array([segment]), 8, 0);
      }
    }
  }

  function appendUnalignedBits(unalignedBits, size, offset) {
    if (size === 0) {
      return;
    }

    const byteSize = Math.trunc(size + 7 / 8);

    const highBitsCount = cursor % 8;
    const lowBitsCount = 8 - highBitsCount;

    let byteIndex = Math.trunc(cursor / 8);

    for (let i = 0; i < byteSize; i++) {
      let byte = bitArrayByteAt(unalignedBits, offset, i);

      // If this is a partial byte then zero out the trailing bits as their
      // content is undefined and shouldn't be included in the output
      if (size < 8) {
        byte >>= 8 - size;
        byte <<= 8 - size;
      }

      // Copy the high bits of the input byte to the low bits of the current
      // output byte
      buffer[byteIndex] |= byte >> highBitsCount;

      let appendedBitsCount = size - Math.max(0, size - lowBitsCount);
      size -= appendedBitsCount;
      cursor += appendedBitsCount;

      if (size === 0) {
        break;
      }

      // Copy the low bits of the input byte to the high bits of the next output
      // byte
      buffer[++byteIndex] = byte << lowBitsCount;
      appendedBitsCount = size - Math.max(0, size - highBitsCount);
      size -= appendedBitsCount;
      cursor += appendedBitsCount;
    }
  }

  return new BitArray(buffer, bitSize);
}

/**
 * Encodes a floating point value into a `Uint8Array`. This is used to create
 * float segments that are part of bit array expressions.
 *
 * @param {number} value
 * @param {number} size
 * @param {boolean} isBigEndian
 * @returns {Uint8Array}
 */
export function sizedFloat(value, size, isBigEndian) {
  if (size !== 16 && size !== 32 && size !== 64) {
    const msg = `Sized floats must be 16-bit, 32-bit or 64-bit, got size of ${size} bits`;
    throw new globalThis.Error(msg);
  }

  if (size === 16) {
    return numberToFp16Uint(value, isBigEndian);
  }

  const buffer = new Uint8Array(size / 8);

  const view = new DataView(buffer.buffer);

  if (size == 64) {
    view.setFloat64(0, value, !isBigEndian);
  } else {
    view.setFloat32(0, value, !isBigEndian);
  }

  return buffer;
}

/**
 * Encodes an integer value into a `Uint8Array`, or a `BitArray` if the size in
 * bits is not a multiple of 8. This is used to create integer segments used in
 * bit array expressions.
 *
 * @param {number} value
 * @param {number} size
 * @param {boolean} isBigEndian
 * @returns {Uint8Array | BitArray}
 */
export function sizedInt(value, size, isBigEndian) {
  if (size <= 0) {
    return new Uint8Array();
  }

  // Fast path when size is 8 bits. This relies on the rounding behavior of the
  // Uint8Array constructor.
  if (size === 8) {
    return new Uint8Array([value]);
  }

  // Fast path when size is less than 8 bits: shift the value up to the high
  // bits
  if (size < 8) {
    value <<= 8 - size;
    return new BitArray(new Uint8Array([value]), size);
  }

  // Allocate output buffer
  const buffer = new Uint8Array(Math.trunc((size + 7) / 8));

  // The number of trailing bits in the final byte. Will be zero if the size is
  // an exact number of bytes.
  const trailingBitsCount = size % 8;

  // The number of unused bits in the final byte of the buffer
  const unusedBitsCount = 8 - trailingBitsCount;

  // For output sizes not exceeding 32 bits the number type is used. For larger
  // output sizes the BigInt type is needed.
  //
  // The code in each of these two paths must be kept in sync.
  if (size <= 32) {
    if (isBigEndian) {
      let i = buffer.length - 1;

      // Set the trailing bits at the end of the output buffer
      if (trailingBitsCount) {
        buffer[i--] = (value << unusedBitsCount) & 0xff;
        value >>= trailingBitsCount;
      }

      for (; i >= 0; i--) {
        buffer[i] = value;
        value >>= 8;
      }
    } else {
      let i = 0;

      const wholeByteCount = Math.trunc(size / 8);
      for (; i < wholeByteCount; i++) {
        buffer[i] = value;
        value >>= 8;
      }

      // Set the trailing bits at the end of the output buffer
      if (trailingBitsCount) {
        buffer[i] = value << unusedBitsCount;
      }
    }
  } else {
    const bigTrailingBitsCount = BigInt(trailingBitsCount);
    const bigUnusedBitsCount = BigInt(unusedBitsCount);

    let bigValue = BigInt(value);

    if (isBigEndian) {
      let i = buffer.length - 1;

      // Set the trailing bits at the end of the output buffer
      if (trailingBitsCount) {
        buffer[i--] = Number(bigValue << bigUnusedBitsCount);
        bigValue >>= bigTrailingBitsCount;
      }

      for (; i >= 0; i--) {
        buffer[i] = Number(bigValue);
        bigValue >>= 8n;
      }
    } else {
      let i = 0;

      const wholeByteCount = Math.trunc(size / 8);
      for (; i < wholeByteCount; i++) {
        buffer[i] = Number(bigValue);
        bigValue >>= 8n;
      }

      // Set the trailing bits at the end of the output buffer
      if (trailingBitsCount) {
        buffer[i] = Number(bigValue << bigUnusedBitsCount);
      }
    }
  }

  // Integers that aren't a whole number of bytes are returned as a BitArray so
  // their size in bits is tracked
  if (trailingBitsCount) {
    return new BitArray(buffer, size);
  }

  return buffer;
}

/**
 * Reads an aligned slice of any size as an integer.
 *
 * @param {BitArray} bitArray
 * @param {number} start
 * @param {number} end
 * @param {boolean} isBigEndian
 * @param {boolean} isSigned
 * @returns {number}
 */
function intFromAlignedSlice(bitArray, start, end, isBigEndian, isSigned) {
  const byteSize = end - start;

  if (byteSize <= 6) {
    return intFromAlignedSliceUsingNumber(
      bitArray.rawBuffer,
      start,
      end,
      isBigEndian,
      isSigned,
    );
  } else {
    return intFromAlignedSliceUsingBigInt(
      bitArray.rawBuffer,
      start,
      end,
      isBigEndian,
      isSigned,
    );
  }
}

/**
 * Reads an aligned slice up to 48 bits in size as an integer. Uses the
 * JavaScript `number` type internally.
 *
 * @param {Uint8Array} buffer
 * @param {number} start
 * @param {number} end
 * @param {boolean} isBigEndian
 * @param {boolean} isSigned
 * @returns {number}
 */
function intFromAlignedSliceUsingNumber(
  buffer,
  start,
  end,
  isBigEndian,
  isSigned,
) {
  const byteSize = end - start;

  let value = 0;

  // Read bytes as an unsigned integer
  if (isBigEndian) {
    for (let i = start; i < end; i++) {
      value *= 256;
      value += buffer[i];
    }
  } else {
    for (let i = end - 1; i >= start; i--) {
      value *= 256;
      value += buffer[i];
    }
  }

  // For signed integers, if the high bit is set reinterpret as two's
  // complement
  if (isSigned) {
    const highBit = 2 ** (byteSize * 8 - 1);
    if (value >= highBit) {
      value -= highBit * 2;
    }
  }

  return value;
}

/**
 * Reads an aligned slice of any size as an integer. Uses the JavaScript
 * `BigInt` type internally.
 *
 * @param {Uint8Array} buffer
 * @param {number} start
 * @param {number} end
 * @param {boolean} isBigEndian
 * @param {boolean} isSigned
 * @returns {number}
 */
function intFromAlignedSliceUsingBigInt(
  buffer,
  start,
  end,
  isBigEndian,
  isSigned,
) {
  const byteSize = end - start;

  let value = 0n;

  // Read bytes as an unsigned integer value
  if (isBigEndian) {
    for (let i = start; i < end; i++) {
      value *= 256n;
      value += BigInt(buffer[i]);
    }
  } else {
    for (let i = end - 1; i >= start; i--) {
      value *= 256n;
      value += BigInt(buffer[i]);
    }
  }

  // For signed integers, if the high bit is set reinterpret as two's
  // complement
  if (isSigned) {
    const highBit = 1n << BigInt(byteSize * 8 - 1);
    if (value >= highBit) {
      value -= highBit * 2n;
    }
  }

  // Convert the result into a JS number. This may cause quantizing/error on
  // values outside JavaScript's safe integer range.
  return Number(value);
}

/**
 * Reads an unaligned slice up to 53 bits in size as an integer. Uses the
 * JavaScript `number` type internally.
 *
 * This function assumes that the slice crosses at least one byte boundary in
 * the input.
 *
 * @param {Uint8Array} buffer
 * @param {number} start
 * @param {number} end
 * @param {boolean} isBigEndian
 * @param {boolean} isSigned
 * @returns {number}
 */
function intFromUnalignedSliceUsingNumber(
  buffer,
  start,
  end,
  isBigEndian,
  isSigned,
) {
  const isStartByteAligned = start % 8 === 0;

  let size = end - start;
  let byteIndex = Math.trunc(start / 8);

  let value = 0;

  if (isBigEndian) {
    // Read any leading bits
    if (!isStartByteAligned) {
      const leadingBitsCount = 8 - (start % 8);
      value = buffer[byteIndex++] & ((1 << leadingBitsCount) - 1);
      size -= leadingBitsCount;
    }

    // Read any whole bytes
    while (size >= 8) {
      value *= 256;
      value += buffer[byteIndex++];
      size -= 8;
    }

    // Read any trailing bits
    if (size > 0) {
      value *= 2 ** size;
      value += buffer[byteIndex] >> (8 - size);
    }
  } else {
    // For little endian, if the start is aligned then whole bytes can be read
    // directly out of the input array, with the trailing bits handled at the
    // end
    if (isStartByteAligned) {
      let size = end - start;
      let scale = 1;

      // Read whole bytes
      while (size >= 8) {
        value += buffer[byteIndex++] * scale;
        scale *= 256;
        size -= 8;
      }

      // Read trailing bits
      value += (buffer[byteIndex] >> (8 - size)) * scale;
    } else {
      // Read little endian data where the start is not byte-aligned. This is
      // done by reading whole bytes that cross a byte boundary in the input
      // data, then reading any trailing bits.

      const highBitsCount = start % 8;
      const lowBitsCount = 8 - highBitsCount;

      let size = end - start;
      let scale = 1;

      // Extract whole bytes
      while (size >= 8) {
        const byte =
          (buffer[byteIndex] << highBitsCount) |
          (buffer[byteIndex + 1] >> lowBitsCount);

        value += (byte & 0xff) * scale;

        scale *= 256;
        size -= 8;
        byteIndex++;
      }

      // Read any trailing bits. These trailing bits may cross a byte boundary
      // in the input buffer.
      if (size > 0) {
        const lowBitsUsed = size - Math.max(0, size - lowBitsCount);

        let trailingByte =
          (buffer[byteIndex] & ((1 << lowBitsCount) - 1)) >>
          (lowBitsCount - lowBitsUsed);

        size -= lowBitsUsed;

        if (size > 0) {
          trailingByte *= 2 ** size;
          trailingByte += buffer[byteIndex + 1] >> (8 - size);
        }

        value += trailingByte * scale;
      }
    }
  }

  // For signed integers, if the high bit is set reinterpret as two's
  // complement
  if (isSigned) {
    const highBit = 2 ** (end - start - 1);
    if (value >= highBit) {
      value -= highBit * 2;
    }
  }

  return value;
}

/**
 * Reads an unaligned slice of any size as an integer. Uses the JavaScript
 * `BigInt` type internally.
 *
 * This function assumes that the slice crosses at least one byte boundary in
 * the input.
 *
 * @param {Uint8Array} buffer
 * @param {number} start
 * @param {number} end
 * @param {boolean} isBigEndian
 * @param {boolean} isSigned
 * @returns {number}
 */
function intFromUnalignedSliceUsingBigInt(
  buffer,
  start,
  end,
  isBigEndian,
  isSigned,
) {
  const isStartByteAligned = start % 8 === 0;

  let size = end - start;
  let byteIndex = Math.trunc(start / 8);

  let value = 0n;

  if (isBigEndian) {
    // Read any leading bits
    if (!isStartByteAligned) {
      const leadingBitsCount = 8 - (start % 8);
      value = BigInt(buffer[byteIndex++] & ((1 << leadingBitsCount) - 1));
      size -= leadingBitsCount;
    }

    // Read any whole bytes
    while (size >= 8) {
      value *= 256n;
      value += BigInt(buffer[byteIndex++]);
      size -= 8;
    }

    // Read any trailing bits
    if (size > 0) {
      value <<= BigInt(size);
      value += BigInt(buffer[byteIndex] >> (8 - size));
    }
  } else {
    // For little endian, if the start is aligned then whole bytes can be read
    // directly out of the input array, with the trailing bits handled at the
    // end
    if (isStartByteAligned) {
      let size = end - start;
      let shift = 0n;

      // Read whole bytes
      while (size >= 8) {
        value += BigInt(buffer[byteIndex++]) << shift;
        shift += 8n;
        size -= 8;
      }

      // Read trailing bits
      value += BigInt(buffer[byteIndex] >> (8 - size)) << shift;
    } else {
      // Read little endian data where the start is not byte-aligned. This is
      // done by reading whole bytes that cross a byte boundary in the input
      // data, then reading any trailing bits.

      const highBitsCount = start % 8;
      const lowBitsCount = 8 - highBitsCount;

      let size = end - start;
      let shift = 0n;

      // Extract whole bytes
      while (size >= 8) {
        const byte =
          (buffer[byteIndex] << highBitsCount) |
          (buffer[byteIndex + 1] >> lowBitsCount);

        value += BigInt(byte & 0xff) << shift;

        shift += 8n;
        size -= 8;
        byteIndex++;
      }

      // Read any trailing bits. These trailing bits may cross a byte boundary
      // in the input buffer.
      if (size > 0) {
        const lowBitsUsed = size - Math.max(0, size - lowBitsCount);

        let trailingByte =
          (buffer[byteIndex] & ((1 << lowBitsCount) - 1)) >>
          (lowBitsCount - lowBitsUsed);

        size -= lowBitsUsed;

        if (size > 0) {
          trailingByte <<= size;
          trailingByte += buffer[byteIndex + 1] >> (8 - size);
        }

        value += BigInt(trailingByte) << shift;
      }
    }
  }

  // For signed integers, if the high bit is set reinterpret as two's
  // complement
  if (isSigned) {
    const highBit = 2n ** BigInt(end - start - 1);
    if (value >= highBit) {
      value -= highBit * 2n;
    }
  }

  // Convert the result into a JS number. This may cause quantizing/error on
  // values outside JavaScript's safe integer range.
  return Number(value);
}

/**
 * Interprets a 16-bit unsigned integer value as a 16-bit floating point value.
 *
 * @param {number} intValue
 * @returns {number}
 */
function fp16UintToNumber(intValue) {
  const sign = intValue >= 0x8000 ? -1 : 1;
  const exponent = (intValue & 0x7c00) >> 10;
  const fraction = intValue & 0x03ff;

  let value;
  if (exponent === 0) {
    value = 6.103515625e-5 * (fraction / 0x400);
  } else if (exponent === 0x1f) {
    value = fraction === 0 ? Infinity : NaN;
  } else {
    value = Math.pow(2, exponent - 15) * (1 + fraction / 0x400);
  }

  return sign * value;
}

/**
 * Converts a floating point number to bytes for a 16-bit floating point value.
 *
 * @param {number} intValue
 * @param {boolean} isBigEndian
 * @returns {Uint8Array}
 */
function numberToFp16Uint(value, isBigEndian) {
  const buffer = new Uint8Array(2);

  if (isNaN(value)) {
    buffer[1] = 0x7e;
  } else if (value === Infinity) {
    buffer[1] = 0x7c;
  } else if (value === -Infinity) {
    buffer[1] = 0xfc;
  } else if (value === 0) {
    // 0 === -0, so we need to do an additional check here
    if (isNegativeZero(value)) {
      buffer[1] = 128;
    }
    // Otherwise, both values are already zero
  } else {
    const sign = value < 0 ? 1 : 0;
    value = Math.abs(value);

    let exponent = Math.floor(Math.log2(value));
    let fraction = value / Math.pow(2, exponent) - 1;

    exponent += 15;

    if (exponent <= 0) {
      exponent = 0;
      fraction = value / Math.pow(2, -14);
    } else if (exponent >= 31) {
      exponent = 31;
      fraction = 0;
    }

    fraction = Math.round(fraction * 1024);

    buffer[1] =
      (sign << 7) | ((exponent & 0x1f) << 2) | ((fraction >> 8) & 0x03);
    buffer[0] = fraction & 0xff;
  }

  if (isBigEndian) {
    const a = buffer[0];
    buffer[0] = buffer[1];
    buffer[1] = a;
  }

  return buffer;
}

/**
 * Returns whether or not a value is `-0`, since this cannot be checked using
 * equality.
 *
 * @param {number} value
 * @returns {boolean}
 */
function isNegativeZero(value) {
  // One of the few differences between 0 and -0 is that division by -0 returns
  // -Infinity rather than Infinity.
  return 1 / value === -Infinity;
}

/**
 * Throws an exception if the given start and end values are out of bounds for
 * a bit array.
 *
 * @param {BitArray} bitArray
 * @param {number} start
 * @param {number} end
 */
function bitArrayValidateRange(bitArray, start, end) {
  if (
    start < 0 ||
    start > bitArray.bitSize ||
    end < start ||
    end > bitArray.bitSize
  ) {
    const msg =
      `Invalid bit array slice: start = ${start}, end = ${end}, ` +
      `bit size = ${bitArray.bitSize}`;
    throw new globalThis.Error(msg);
  }
}

/** @type {TextEncoder | undefined} */
let utf8Encoder;

/**
 * Returns the UTF-8 bytes for a string.
 *
 * @param {string} string
 * @returns {Uint8Array}
 */
export function stringBits(string) {
  utf8Encoder ??= new TextEncoder();
  return utf8Encoder.encode(string);
}

/**
 * Returns the UTF-8 bytes for a single UTF codepoint.
 *
 * @param {UtfCodepoint} codepoint
 * @returns {Uint8Array}
 */
export function codepointBits(codepoint) {
  return stringBits(String.fromCodePoint(codepoint.value));
}

/**
 * Returns the UTF-16 bytes for a string.
 *
 * @param {string} string
 * @param {boolean} isBigEndian
 * @returns {Uint8Array}
 */
export function stringToUtf16(string, isBigEndian) {
  const buffer = new ArrayBuffer(string.length * 2);
  const bufferView = new DataView(buffer);

  for (let i = 0; i < string.length; i++) {
    bufferView.setUint16(i * 2, string.charCodeAt(i), !isBigEndian);
  }

  return new Uint8Array(buffer);
}

/**
 * Returns the UTF-16 bytes for a single UTF codepoint.
 *
 * @param {UtfCodepoint} codepoint
 * @param {boolean} isBigEndian
 * @returns {Uint8Array}
 */
export function codepointToUtf16(codepoint, isBigEndian) {
  return stringToUtf16(String.fromCodePoint(codepoint.value), isBigEndian);
}

/**
 * Returns the UTF-32 bytes for a string.
 *
 * @param {string} string
 * @param {boolean} isBigEndian
 * @returns {Uint8Array}
 */
export function stringToUtf32(string, isBigEndian) {
  const buffer = new ArrayBuffer(string.length * 4);
  const bufferView = new DataView(buffer);
  let length = 0;

  for (let i = 0; i < string.length; i++) {
    const codepoint = string.codePointAt(i);

    bufferView.setUint32(length * 4, codepoint, !isBigEndian);
    length++;

    if (codepoint > 0xffff) {
      i++;
    }
  }

  return new Uint8Array(buffer.slice(0, length * 4));
}

/**
 * Returns the UTF-32 bytes for a single UTF codepoint.
 *
 * @param {UtfCodepoint} codepoint
 * @param {boolean} isBigEndian
 * @returns {Uint8Array}
 */
export function codepointToUtf32(codepoint, isBigEndian) {
  return stringToUtf32(String.fromCodePoint(codepoint.value), isBigEndian);
}

export class Result extends CustomType {
  static isResult(data) {
    return data instanceof Result;
  }
}

export class Ok extends Result {
  constructor(value) {
    super();
    this[0] = value;
  }

  isOk() {
    return true;
  }
}
export const Result$Ok = (value) => new Ok(value);
export const Result$isOk = (value) => value instanceof Ok;
export const Result$Ok$0 = (value) => value[0];

export class Error extends Result {
  constructor(detail) {
    super();
    this[0] = detail;
  }

  isOk() {
    return false;
  }
}
export const Result$Error = (detail) => new Error(detail);
export const Result$isError = (value) => value instanceof Error;
export const Result$Error$0 = (value) => value[0];

export function isEqual(x, y) {
  let values = [x, y];

  while (values.length) {
    let a = values.pop();
    let b = values.pop();
    if (a === b) continue;

    if (!isObject(a) || !isObject(b)) return false;
    let unequal =
      !structurallyCompatibleObjects(a, b) ||
      unequalDates(a, b) ||
      unequalBuffers(a, b) ||
      unequalArrays(a, b) ||
      unequalMaps(a, b) ||
      unequalSets(a, b) ||
      unequalRegExps(a, b);
    if (unequal) return false;

    const proto = Object.getPrototypeOf(a);
    if (proto !== null && typeof proto.equals === "function") {
      try {
        if (a.equals(b)) continue;
        else return false;
      } catch { }
    }

    let [keys, get] = getters(a);
    const ka = keys(a);
    const kb = keys(b);
    if (ka.length !== kb.length) return false;
    for (let k of ka) {
      values.push(get(a, k), get(b, k));
    }
  }

  return true;
}

function getters(object) {
  if (object instanceof Map) {
    return [(x) => x.keys(), (x, y) => x.get(y)];
  } else {
    let extra = object instanceof globalThis.Error ? ["message"] : [];
    return [(x) => [...extra, ...Object.keys(x)], (x, y) => x[y]];
  }
}

function unequalDates(a, b) {
  return a instanceof Date && (a > b || a < b);
}

function unequalBuffers(a, b) {
  return (
    !(a instanceof BitArray) &&
    a.buffer instanceof ArrayBuffer &&
    a.BYTES_PER_ELEMENT &&
    !(a.byteLength === b.byteLength && a.every((n, i) => n === b[i]))
  );
}

function unequalArrays(a, b) {
  return Array.isArray(a) && a.length !== b.length;
}

function unequalMaps(a, b) {
  return a instanceof Map && a.size !== b.size;
}

function unequalSets(a, b) {
  return (
    a instanceof Set && (a.size != b.size || [...a].some((e) => !b.has(e)))
  );
}

function unequalRegExps(a, b) {
  return a instanceof RegExp && (a.source !== b.source || a.flags !== b.flags);
}

function isObject(a) {
  return typeof a === "object" && a !== null;
}

function structurallyCompatibleObjects(a, b) {
  if (typeof a !== "object" && typeof b !== "object" && (!a || !b))
    return false;

  let nonstructural = [Promise, WeakSet, WeakMap, Function];
  if (nonstructural.some((c) => a instanceof c)) return false;

  return a.constructor === b.constructor;
}

export function remainderInt(a, b) {
  if (b === 0) {
    return 0;
  } else {
    return a % b;
  }
}

export function divideInt(a, b) {
  return Math.trunc(divideFloat(a, b));
}

export function divideFloat(a, b) {
  if (b === 0) {
    return 0;
  } else {
    return a / b;
  }
}

export function makeError(variant, file, module, line, fn, message, extra) {
  let error = new globalThis.Error(message);
  error.gleam_error = variant;
  error.file = file;
  error.module = module;
  error.line = line;
  error.function = fn;
  // TODO: Remove this with Gleam v2.0.0
  error.fn = fn;
  for (let k in extra) error[k] = extra[k];
  return error;
}
