export type MarkdownFenceCharacter = '`' | '~'

export interface MarkdownFence {
  character: MarkdownFenceCharacter
  length: number
}

export interface MarkdownFenceScanOptions {
  closingMustEndLine?: boolean
  maxLeadingSpaces?: number | null
}

const DEFAULT_MAX_LEADING_SPACES = 3

function fencePrefixPattern(options: MarkdownFenceScanOptions): RegExp {
  return options.maxLeadingSpaces === null
    ? /^(\s*)(`{3,}|~{3,})/u
    : /^( *)(`{3,}|~{3,})/u
}

function hasAllowedIndent(match: RegExpExecArray, options: MarkdownFenceScanOptions): boolean {
  if (options.maxLeadingSpaces === null) return true
  return (match[1]?.length ?? 0) <= (options.maxLeadingSpaces ?? DEFAULT_MAX_LEADING_SPACES)
}

function readFenceMatch(line: string, options: MarkdownFenceScanOptions): RegExpExecArray | null {
  const match = fencePrefixPattern(options).exec(line)
  return match && hasAllowedIndent(match, options) ? match : null
}

function fenceFromMatch(match: RegExpExecArray): MarkdownFence {
  const fence = match[2] ?? ''
  return {
    character: fence.charAt(0) as MarkdownFenceCharacter,
    length: fence.length,
  }
}

export function readMarkdownFence(
  line: string,
  options: MarkdownFenceScanOptions = {},
): MarkdownFence | null {
  const match = readFenceMatch(line, options)
  return match ? fenceFromMatch(match) : null
}

function closingTailIsAllowed(
  line: string,
  match: RegExpExecArray,
  options: MarkdownFenceScanOptions,
): boolean {
  if (options.closingMustEndLine === false) return true
  return /^[ \t]*$/u.test(line.slice(match[0].length))
}

function isClosingMarkdownFence(
  line: string,
  opening: MarkdownFence,
  options: MarkdownFenceScanOptions,
): boolean {
  const match = readFenceMatch(line, options)
  if (!match) return false

  const closing = fenceFromMatch(match)
  return closing.character === opening.character
    && closing.length >= opening.length
    && closingTailIsAllowed(line, match, options)
}

export function advanceMarkdownFence(
  line: string,
  current: MarkdownFence | null,
  options: MarkdownFenceScanOptions = {},
): MarkdownFence | null {
  if (current) {
    return isClosingMarkdownFence(line, current, options) ? null : current
  }
  return readMarkdownFence(line, options)
}

export function isInsideMarkdownFence(
  markdownBeforeCursor: string,
  options: MarkdownFenceScanOptions = {},
): boolean {
  let fence: MarkdownFence | null = null

  for (const line of markdownBeforeCursor.split(/\r?\n/u)) {
    fence = advanceMarkdownFence(line, fence, options)
  }

  return fence !== null
}
