import {
  buildCodePointOffsetMap,
  type CodePointOffsetMap,
} from "./codepoint-to-utf16";
import { buildUtf8OffsetMap, type Utf8OffsetMap } from "./utf8-to-utf16";

export interface EditorOffsetMap {
  readonly source: string;
  readonly utf8: Utf8OffsetMap;
  readonly codePoints: CodePointOffsetMap;
  readonly lineStartsUtf16: readonly number[];
}

export function buildEditorOffsetMap(source: string): EditorOffsetMap {
  const lineStartsUtf16 = [0];
  for (let index = 0; index < source.length; index += 1) {
    const current = source.charCodeAt(index);
    if (current === 13 && source.charCodeAt(index + 1) === 10) {
      lineStartsUtf16.push(index + 2);
      index += 1;
    } else if (
      current === 10 ||
      current === 13 ||
      current === 0x2028 ||
      current === 0x2029
    ) {
      lineStartsUtf16.push(index + 1);
    }
  }
  return {
    source,
    utf8: buildUtf8OffsetMap(source),
    codePoints: buildCodePointOffsetMap(source),
    lineStartsUtf16,
  };
}

export function lineAndColumnAt(
  map: EditorOffsetMap,
  offsetUtf16: number,
): { readonly line: number; readonly column: number } {
  if (offsetUtf16 < 0 || offsetUtf16 > map.source.length) {
    throw new RangeError(`UTF-16 offset ${offsetUtf16} is outside the input.`);
  }
  let low = 0;
  let high = map.lineStartsUtf16.length - 1;
  while (low < high) {
    const middle = Math.ceil((low + high) / 2);
    if ((map.lineStartsUtf16[middle] ?? 0) <= offsetUtf16) {
      low = middle;
    } else {
      high = middle - 1;
    }
  }
  const start = map.lineStartsUtf16[low] ?? 0;
  return { line: low + 1, column: offsetUtf16 - start + 1 };
}
