import { detectLatexLineEnding } from './line-endings';
import type { SourceTextEdit } from './text-edit';

/**
 * Canonical hard-break spelling used by every Learnitive Visual insertion path.
 * The empty group makes the control word lexically self-terminating, so text
 * typed immediately afterwards can never turn `\\newline` into another
 * command such as `\\newlinefoo`.
 */
export const CANONICAL_LATEX_HARD_BREAK = '\\newline{}' as const;

/** Legacy explicit paragraph command accepted when parsing imported/source-authored LaTeX. */
export const CANONICAL_LATEX_PARAGRAPH_BREAK = '\\par' as const;

/** Canonical source spelling generated by normal Visual-mode paragraph Enter. */
export const CANONICAL_LATEX_PARAGRAPH_SEPARATOR = '\n\n' as const;

/**
 * Stable source anchor for an editable body that contains only horizontal
 * padding. Deliberately stop before a newline: crossing a line boundary would
 * move an empty list item/cell caret onto structural syntax owned by its
 * parent.
 */
export function canonicalEmptySourceAnchor(
  source: string,
  from: number,
  to: number,
): number {
  let anchor = Math.max(0, Math.min(source.length, from));
  const limit = Math.max(anchor, Math.min(source.length, to));
  while (anchor < limit && /[ \t\r]/.test(source[anchor] ?? '')) anchor += 1;
  return anchor;
}

export type SourceBackedBlockDeletion = {
  edit: SourceTextEdit;
  focusOffset: number;
};

/**
 * Remove a block-like source range (or insert a zero-width gap) while guaranteeing one canonical editable
 * blank Visual row. Existing line feeds immediately outside the range count
 * toward the required three-newline run, so deleting an environment does not
 * multiply ordinary surrounding whitespace into several empty rows.
 */
export function createSourceBackedBlockDeletion(
  source: string,
  range: { from: number; to: number },
): SourceBackedBlockDeletion | undefined {
  if (
    !Number.isInteger(range.from) ||
    !Number.isInteger(range.to) ||
    range.from < 0 ||
    range.to < range.from ||
    range.to > source.length
  ) {
    return undefined;
  }

  const lineEnding = detectLatexLineEnding(source);
  let leftNewlines = 0;
  let leftBytes = 0;
  for (let at = range.from; at >= lineEnding.length && source.slice(at - lineEnding.length, at) === lineEnding;) {
    leftNewlines += 1;
    leftBytes += lineEnding.length;
    at -= lineEnding.length;
  }
  let rightNewlines = 0;
  for (let at = range.to; at + lineEnding.length <= source.length && source.slice(at, at + lineEnding.length) === lineEnding;) {
    rightNewlines += 1;
    at += lineEnding.length;
  }

  const missing = Math.max(0, 3 - leftNewlines - rightNewlines);
  const text = lineEnding.repeat(missing);
  const gapStart = range.from - leftBytes;
  return {
    edit: { from: range.from, to: range.to, text },
    // The canonical blank-row anchor is after the second complete line ending
    // in the surrounding run. This stays correct for both LF and CRLF source.
    focusOffset: gapStart + lineEnding.length * 2,
  };
}

export type LatexInlineWrapperRange = {
  sourceFrom: number;
  sourceTo: number;
  bodyFrom: number;
  bodyTo: number;
};

export type LatexInlineSplitMutation = {
  edit: SourceTextEdit;
  focusOffset: number;
};

/**
 * Create one source-safe structural split at a Visual caret/selection.
 *
 * Plain source-backed text is replaced directly. When the caret lives inside
 * a simple inline wrapper (for example `\\textbf{...}` or `\\href{...}{...}`)
 * the wrapper is split into two balanced copies around the structural token.
 * This keeps the canonical LaTeX valid instead of injecting paragraph/list
 * structure into the middle of a required argument.
 */
export function createLatexInlineSplitMutation(
  source: string,
  from: number,
  to: number,
  separator: string,
  wrapper?: LatexInlineWrapperRange,
): LatexInlineSplitMutation | undefined {
  if (
    !Number.isInteger(from) ||
    !Number.isInteger(to) ||
    from < 0 ||
    to < from ||
    to > source.length
  ) {
    return undefined;
  }

  if (!wrapper) {
    return {
      edit: { from, to, text: separator },
      focusOffset: from + separator.length,
    };
  }

  const { sourceFrom, sourceTo, bodyFrom, bodyTo } = wrapper;
  if (
    !Number.isInteger(sourceFrom) ||
    !Number.isInteger(sourceTo) ||
    !Number.isInteger(bodyFrom) ||
    !Number.isInteger(bodyTo) ||
    sourceFrom < 0 ||
    sourceTo < sourceFrom ||
    sourceTo > source.length ||
    bodyFrom < sourceFrom ||
    bodyTo < bodyFrom ||
    bodyTo > sourceTo ||
    from < bodyFrom ||
    to > bodyTo
  ) {
    return undefined;
  }

  const prefix = source.slice(sourceFrom, bodyFrom);
  const suffix = source.slice(bodyTo, sourceTo);
  // A wrapper without any source syntax around its body is equivalent to
  // plain text; avoid manufacturing duplicate empty wrappers.
  if (!prefix && !suffix) {
    return {
      edit: { from, to, text: separator },
      focusOffset: from + separator.length,
    };
  }

  const leftBody = source.slice(bodyFrom, from);
  const rightBody = source.slice(to, bodyTo);
  const leftWrapped = `${prefix}${leftBody}${suffix}`;
  const rightPrefix = prefix;
  const replacement = `${leftWrapped}${separator}${rightPrefix}${rightBody}${suffix}`;

  return {
    edit: { from: sourceFrom, to: sourceTo, text: replacement },
    // Put the caret at the start of the right-hand wrapper body. This remains
    // source-addressable even when either side is empty.
    focusOffset: sourceFrom + leftWrapped.length + separator.length + rightPrefix.length,
  };
}
