export interface Utf8OffsetMap {
  readonly byteLength: number;
  readonly utf16Length: number;
  readonly byteToUtf16: ReadonlyMap<number, number>;
  readonly utf16ToByte: ReadonlyMap<number, number>;
  readonly containsLoneSurrogate: boolean;
}

function utf8Length(codePoint: number): number {
  if (codePoint <= 0x7f) return 1;
  if (codePoint <= 0x7ff) return 2;
  if (codePoint <= 0xffff) return 3;
  return 4;
}

export function buildUtf8OffsetMap(value: string): Utf8OffsetMap {
  const byteToUtf16 = new Map<number, number>([[0, 0]]);
  const utf16ToByte = new Map<number, number>([[0, 0]]);
  let byteOffset = 0;
  let utf16Offset = 0;
  let containsLoneSurrogate = false;
  for (const character of value) {
    const codePoint = character.codePointAt(0);
    if (codePoint === undefined) continue;
    if (codePoint >= 0xd800 && codePoint <= 0xdfff) {
      containsLoneSurrogate = true;
    }
    const utf16Width = character.length;
    const byteWidth = utf8Length(codePoint);
    byteOffset += byteWidth;
    utf16Offset += utf16Width;
    byteToUtf16.set(byteOffset, utf16Offset);
    utf16ToByte.set(utf16Offset, byteOffset);
  }
  return {
    byteLength: byteOffset,
    utf16Length: utf16Offset,
    byteToUtf16,
    utf16ToByte,
    containsLoneSurrogate,
  };
}

export function utf8ByteToUtf16(map: Utf8OffsetMap, offset: number): number {
  const normalized = map.byteToUtf16.get(offset);
  if (normalized === undefined) {
    throw new RangeError(
      `UTF-8 byte offset ${offset} is outside the input or inside an encoded sequence.`,
    );
  }
  return normalized;
}

export function utf16ToUtf8Byte(map: Utf8OffsetMap, offset: number): number {
  const native = map.utf16ToByte.get(offset);
  if (native === undefined) {
    throw new RangeError(
      `UTF-16 offset ${offset} is outside the input or splits a surrogate pair.`,
    );
  }
  return native;
}
