import type { Root, Element, ElementContent, Text } from 'hast'
import type { Plugin } from 'unified'
import { visit } from 'unist-util-visit'

/**
 * Checks whether a line starts with a colon marker (optionally preceded by
 * whitespace) and, if so, strips it and returns the remaining content.
 * @param line The line (array of ElementContent) to check.
 * @returns Whether the line is a colon-prefixed definition line, and its content with the marker stripped.
 */
const stripLeadingColon = (
  line: ElementContent[],
): { isDefinitionLine: boolean; content: ElementContent[] } => {
  const [first, ...rest] = line
  if (first?.type === 'text') {
    const match = first.value.match(/^[ \t]*:[ \t]*/)
    if (match) {
      const remainder = first.value.slice(match[0].length)
      return {
        isDefinitionLine: true,
        content: remainder ? [{ type: 'text', value: remainder } as Text, ...rest] : rest,
      }
    }
  }
  return { isDefinitionLine: false, content: line }
}

/**
 * Splits an array of ElementContent nodes into lines based on text nodes containing newlines and <br> elements.
 * @param nodes The array of ElementContent nodes to split into lines.
 * @returns An array of lines, where each line is an array of ElementContent nodes.
 */
const splitIntoLines = (nodes: ElementContent[]): ElementContent[][] => {
  const lines: ElementContent[][] = [[]]

  for (const child of nodes) {
    if (child.type === 'text') {
      const segments = child.value.split('\n')
      segments.forEach((segment, i) => {
        if (i > 0) lines.push([])
        if (segment.length > 0) {
          lines[lines.length - 1].push({ type: 'text', value: segment } as Text)
        }
      })
    } else if (child.type === 'element' && child.tagName === 'br') {
      lines.push([])
    } else {
      lines[lines.length - 1].push(child)
    }
  }

  return lines
}

/**
 * A rehype plugin that transforms paragraphs shaped like:
 *
 *   Term
 *   : Definition A for term.
 *   : Definition B for term, on a new line.
 *
 * into a <dl> element with a <dt> for the term and a <dd> per colon-prefixed
 * line. The term must sit on its own line, and every subsequent non-empty
 * line must start with ":" — otherwise the paragraph is left untouched, so
 * ordinary sentences containing a colon (e.g. "My passions: A, B and C")
 * are never affected.
 */
export const rehypeDefinition: Plugin<[], Root> = () => (tree: Root) => {
  visit(tree, 'element', (node: Element, index, parent) => {
    if (!parent || typeof index !== 'number') return
    if (node.tagName !== 'p') return

    const lines = splitIntoLines(node.children)

    // Need at least a term line and one definition line.
    if (lines.length < 2) return

    const [termLine, ...restLines] = lines

    if (termLine.length === 0) return
    // The term line itself must not be a colon-prefixed line.
    if (stripLeadingColon(termLine).isDefinitionLine) return

    const definitions: ElementContent[][] = []

    for (const line of restLines) {
      if (line.length === 0) continue
      const { isDefinitionLine, content } = stripLeadingColon(line)
      // Any non-colon-prefixed line means this isn't the definition format.
      if (!isDefinitionLine) return
      if (content.length > 0) definitions.push(content)
    }

    if (definitions.length === 0) return

    const dlNode: Element = {
      type: 'element',
      tagName: 'dl',
      properties: {},
      children: [
        {
          type: 'element',
          tagName: 'dt',
          properties: {},
          children: termLine,
        },
        ...definitions.map(
          (defChildren): Element => ({
            type: 'element',
            tagName: 'dd',
            properties: {},
            children: defChildren,
          }),
        ),
      ],
    }

    parent.children[index] = dlNode
  })
}
