export interface LineAndColumn {
  readonly line: number;
  readonly column: number;
}

/**
 * Resolves only requested offsets in one source scan. Memory therefore follows
 * the bounded result set rather than the number of newlines in a large subject.
 */
export function lineAndColumnsAt(
  source: string,
  requestedOffsetsUtf16: readonly number[],
): ReadonlyMap<number, LineAndColumn> {
  const offsets = [...new Set(requestedOffsetsUtf16)].sort(
    (left, right) => left - right,
  );
  for (const offset of offsets) {
    if (!Number.isSafeInteger(offset) || offset < 0 || offset > source.length) {
      throw new RangeError(`UTF-16 offset ${offset} is outside the input.`);
    }
  }

  const result = new Map<number, LineAndColumn>();
  let sourceOffset = 0;
  let line = 1;
  let lineStart = 0;
  for (const target of offsets) {
    while (sourceOffset < target) {
      const current = source.charCodeAt(sourceOffset);
      const isCrLfStart =
        current === 13 && source.charCodeAt(sourceOffset + 1) === 10;
      const isLineEnd =
        current === 10 ||
        (current === 13 && !isCrLfStart) ||
        current === 0x2028 ||
        current === 0x2029;
      if (isLineEnd) {
        line += 1;
        lineStart = sourceOffset + 1;
      }
      sourceOffset += 1;
    }
    result.set(target, {
      line,
      column: target - lineStart + 1,
    });
  }
  return result;
}
