export type LatexLineEnding = '\n' | '\r\n';

export function detectLatexLineEnding(source: string): LatexLineEnding {
  // Most Learnitive projects use LF. Avoid a JavaScript character-by-character
  // scan on every editor splice when CR is not present at all. Native
  // indexOf is dramatically cheaper for large manuscripts and keeps the
  // ordinary LF path much cheaper before the editor-level CRLF cache applies.
  if (source.indexOf('\r') < 0) return '\n';
  let crlf = 0;
  let lfOnly = 0;
  for (let index = 0; index < source.length; index += 1) {
    if (source.charCodeAt(index) !== 10) continue;
    if (index > 0 && source.charCodeAt(index - 1) === 13) crlf += 1;
    else lfOnly += 1;
  }
  return crlf > lfOnly ? '\r\n' : '\n';
}

export function hasMixedLatexLineEndings(source: string): boolean {
  let crlf = 0;
  let lfOnly = 0;
  let bareCr = 0;
  for (let index = 0; index < source.length; index += 1) {
    const code = source.charCodeAt(index);
    if (code === 10) {
      if (index > 0 && source.charCodeAt(index - 1) === 13) crlf += 1;
      else lfOnly += 1;
    } else if (code === 13 && source.charCodeAt(index + 1) !== 10) {
      bareCr += 1;
    }
  }
  return bareCr > 0 || (crlf > 0 && lfOnly > 0);
}

export function normalizeLatexLineEndings(
  source: string,
  lineEnding: LatexLineEnding = detectLatexLineEnding(source),
): string {
  return source.replace(/\r\n?|\n/gu, '\n').replaceAll('\n', lineEnding);
}

/**
 * CodeMirror's Text model uses LF positions. Learnitive's canonical source may
 * preserve CRLF. This converts a canonical source offset into the equivalent
 * LF-normalized editor offset without rewriting the canonical document.
 */
export function canonicalOffsetToEditorOffset(source: string, offset: number): number {
  const target = Math.max(0, Math.min(offset, source.length));
  if (source.indexOf('\r') < 0) return target;
  let editor = 0;
  for (let index = 0; index < target; index += 1) {
    if (source.charCodeAt(index) === 13 && source.charCodeAt(index + 1) === 10) continue;
    editor += 1;
  }
  return editor;
}

/** Convert an LF-normalized CodeMirror offset back into canonical source coordinates. */
export function editorOffsetToCanonicalOffset(source: string, offset: number): number {
  if (source.indexOf('\r') < 0) return Math.max(0, Math.min(offset, source.length));
  const normalizedLength = canonicalOffsetToEditorOffset(source, source.length);
  const target = Math.max(0, Math.min(offset, normalizedLength));
  let editor = 0;
  for (let index = 0; index < source.length; index += 1) {
    if (editor >= target) return index;
    if (source.charCodeAt(index) === 13 && source.charCodeAt(index + 1) === 10) continue;
    editor += 1;
  }
  return source.length;
}

export function latexSourceToEditorText(source: string): string {
  return source.indexOf('\r') < 0 ? source : source.replace(/\r\n?/gu, '\n');
}

export function editorInsertedTextToCanonical(source: string, inserted: string): string {
  // Ordinary keystrokes contain no newline and therefore need no document-wide
  // line-ending detection. This removes one full-source scan per typed key.
  if (!/[\r\n]/u.test(inserted)) return inserted;
  return normalizeLatexLineEndings(inserted, detectLatexLineEnding(source));
}
