import { compileSafeUserRegex } from './safeRegex'

export interface EditorFindOptions {
  caseSensitive: boolean
  regex: boolean
}

export interface EditorFindMatch {
  from: number
  text: string
  to: number
}

export interface EditorFindResult {
  error: string | null
  matches: EditorFindMatch[]
}

export interface EditorFindChange {
  from: number
  insert: string
  to: number
}

type CompiledPattern =
  | { error: string; pattern: null }
  | { error: null; pattern: RegExp | null }

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

function compileFindPattern(
  query: string,
  options: EditorFindOptions,
  global: boolean,
): CompiledPattern {
  if (query.length === 0) return { error: null, pattern: null }

  const source = options.regex ? query : escapeRegExp(query)
  const flags = `${global ? 'g' : ''}${options.caseSensitive ? '' : 'i'}`

  const compiled = compileSafeUserRegex(source, flags)
  if (compiled.ok) return { error: null, pattern: compiled.pattern }
  return { error: compiled.reason === 'invalid' ? 'Invalid regex' : 'Unsafe regex', pattern: null }
}

export function findEditorMatches(
  documentText: string,
  query: string,
  options: EditorFindOptions,
): EditorFindResult {
  const compiled = compileFindPattern(query, options, true)
  if (compiled.error) return { error: compiled.error, matches: [] }
  if (!compiled.pattern) return { error: null, matches: [] }

  const matches: EditorFindMatch[] = []
  let match = compiled.pattern.exec(documentText)

  while (match !== null) {
    if (match[0].length === 0) {
      return { error: 'Regex must match text', matches: [] }
    }

    matches.push({
      from: match.index,
      text: match[0],
      to: match.index + match[0].length,
    })
    match = compiled.pattern.exec(documentText)
  }

  return { error: null, matches }
}

export function clampEditorFindIndex(index: number, matchCount: number): number {
  if (matchCount <= 0) return 0
  return Math.min(Math.max(index, 0), matchCount - 1)
}

export function nextEditorFindIndex(
  index: number,
  matchCount: number,
  direction: 1 | -1,
): number {
  if (matchCount <= 0) return 0
  return (clampEditorFindIndex(index, matchCount) + direction + matchCount) % matchCount
}

export function replacementForEditorFindMatch(
  match: EditorFindMatch,
  query: string,
  replacement: string,
  options: EditorFindOptions,
): string {
  if (!options.regex) return replacement

  const compiled = compileFindPattern(query, options, false)
  if (!compiled.pattern) return replacement

  return match.text.replace(compiled.pattern, replacement)
}

export function buildEditorFindReplacementChange(
  match: EditorFindMatch,
  query: string,
  replacement: string,
  options: EditorFindOptions,
): EditorFindChange {
  return {
    from: match.from,
    insert: replacementForEditorFindMatch(match, query, replacement, options),
    to: match.to,
  }
}

export function buildEditorFindReplacementChanges(
  matches: readonly EditorFindMatch[],
  query: string,
  replacement: string,
  options: EditorFindOptions,
): EditorFindChange[] {
  return matches.map((match) => (
    buildEditorFindReplacementChange(match, query, replacement, options)
  ))
}
