/**
 * Markdown extensions for Lezer parser.
 *
 * Defines custom inline patterns:
 * - Page references: [[Page Name]] and [[Page Name|Alias]]
 * - Block references: ((block-id))
 * - Tags: #tagname
 * - Highlights: ^^text^^
 */

import { createComarkExtensions } from "@enesis/lezer-comark"
import type {
  BlockContext,
  InlineContext,
  InlineParser,
  LeafBlock,
  LeafBlockParser,
  MarkdownConfig,
} from "@lezer/markdown"

export interface PatternDecorationConfig {
  contentClass: string
  hiddenClass?: string
  showAlias?: boolean
  widgetClass?: string
}

export interface MarkdownPattern {
  name: string
  start: string
  end?: string
  nodeType: string
  decoration?: PatternDecorationConfig
}

function createPatternExtension(patterns: MarkdownPattern[]): MarkdownConfig {
  return {
    defineNodes: patterns.map((p) => ({
      name: p.nodeType,
      block: false,
    })),
    parseInline: patterns.map((pattern) => {
      if (pattern.start === "#" && !pattern.end) {
        const tagName = pattern.nodeType
        return {
          name: tagName,
          before: "Link",
          parse(cx, next, pos) {
            if (next !== 35) return -1

            if (pos > 0) {
              const prev = cx.char(pos - 1)
              if (
                prev != null &&
                prev > 0 &&
                prev !== 32 &&
                prev !== 160 &&
                prev !== 9 &&
                prev !== 10 &&
                prev !== 13
              ) {
                return -1
              }
            }

            let i = pos + 1
            let hasContent = false

            while (i < cx.end) {
              const ch = cx.char(i)
              if (
                ch === 32 ||
                ch === 160 ||
                ch === 9 ||
                ch === 10 ||
                ch === 13 ||
                ch === 35
              ) {
                break
              }
              hasContent = true
              i++
            }

            if (hasContent) {
              return cx.addElement(cx.elt(tagName, pos, i))
            }
            return -1
          },
        }
      }

      return {
        name: pattern.nodeType,
        before: "Link",
        parse(cx, _next, pos) {
          if (!pattern.end) return -1

          const input = cx.slice(pos, pos + pattern.start.length)
          if (input !== pattern.start) return -1

          const endChar = pattern.end.charCodeAt(0)
          let i = pos + pattern.start.length
          let hasContent = false

          while (i < cx.end) {
            const ch = cx.char(i)
            if (ch === endChar) {
              const rest = cx.slice(i, i + pattern.end.length)
              if (rest === pattern.end && hasContent) {
                return cx.addElement(
                  cx.elt(pattern.nodeType, pos, i + pattern.end.length),
                )
              }
              return -1
            }
            hasContent = true
            i++
          }
          return -1
        },
      }
    }),
  }
}

function createInlineMathParser(): InlineParser {
  const nodeType = "InlineMath"
  return {
    name: nodeType,
    before: "Link",
    parse(cx: InlineContext, _next: number, pos: number) {
      if (cx.char(pos) !== 36) return -1 // '$'

      // Don't consume $$ as inline math
      if (cx.char(pos + 1) === 36) return -1

      // Opening $ must not be followed by whitespace
      const afterOpen = cx.char(pos + 1)
      if (
        afterOpen == null ||
        afterOpen === 32 ||
        afterOpen === 9 ||
        afterOpen === 10 ||
        afterOpen === 13
      ) {
        return -1
      }

      let i = pos + 1
      let hasContent = false

      while (i < cx.end) {
        const ch = cx.char(i)
        if (ch === 36) {
          // Closing $ must not be followed by a digit
          const next = cx.char(i + 1)
          if (next != null && next >= 48 && next <= 57) return -1

          if (hasContent) {
            return cx.addElement(cx.elt(nodeType, pos, i + 1))
          }
          return -1
        }
        if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13) {
          hasContent = true
        }
        i++
      }
      return -1
    },
  }
}

// Note: BlockMath is intentionally excluded from defaultPatterns.
// math_block nodes have code: true and are handled by MathBlockView
// (a PM NodeView with contentDOM + sibling preview), not by inline decorations.
export const defaultPatterns: MarkdownPattern[] = [
  {
    name: "PageRef",
    start: "[[",
    end: "]]",
    nodeType: "PageRef",
    decoration: {
      contentClass: "md-page-ref",
      hiddenClass: "md-hidden",
      showAlias: true,
      widgetClass: "md-page-ref-alias",
    },
  },
  {
    name: "BlockRef",
    start: "((",
    end: "))",
    nodeType: "BlockRef",
    decoration: {
      contentClass: "md-block-ref",
      hiddenClass: "md-hidden",
    },
  },
  {
    name: "Tag",
    start: "#",
    nodeType: "Tag",
    decoration: {
      contentClass: "md-tag",
      hiddenClass: "md-hidden",
    },
  },
  {
    name: "Highlight",
    start: "^^",
    end: "^^",
    nodeType: "Highlight",
    decoration: {
      contentClass: "md-highlight",
      hiddenClass: "md-hidden",
    },
  },
]

// ── Task state block extension ─────────────────────────────────────

const TASK_STATES = [
  "TODO",
  "DOING",
  "DONE",
  "LATER",
  "NOW",
  "WAITING",
  "CANCELLED",
]

const TASK_RE = new RegExp(`(?:^|\\b)(${TASK_STATES.join("|")})\\s+`, "i")

class TaskParser implements LeafBlockParser {
  nextLine() {
    return false
  }

  finish(cx: BlockContext, leaf: LeafBlock) {
    const content = leaf.content
    const leading = (content.match(/^\s*/) || [""])[0].length
    const trimmed = content.slice(leading)
    const match = trimmed.match(TASK_RE)
    if (!match) return false
    // biome-ignore lint/style/noNonNullAssertion: match confirmed by TASK_RE above
    const markerLen = match[1]!.length
    // biome-ignore lint/style/noNonNullAssertion: match confirmed by TASK_RE above
    const matchIndex = match.index!
    const afterMarker = matchIndex + match[0]?.length
    const markerStart = leaf.start + leading + matchIndex
    cx.addLeafElement(
      leaf,
      cx.elt("Task", leaf.start, leaf.start + content.length, [
        cx.elt("TaskMarker", markerStart, markerStart + markerLen),
        ...cx.parser.parseInline(
          trimmed.slice(afterMarker),
          markerStart + afterMarker,
        ),
      ]),
    )
    return true
  }
}

const TaskBlock: MarkdownConfig = {
  defineNodes: [{ name: "Task", block: true }, { name: "TaskMarker" }],
  parseBlock: [
    {
      name: "TaskBlock",
      leaf(cx, leaf) {
        return TASK_RE.test(leaf.content) && cx.parentType().name === "Document"
          ? new TaskParser()
          : null
      },
      after: "SetextHeading",
    },
  ],
}

export function createMarkdownExtensions(
  customPatterns: MarkdownPattern[] = [],
): MarkdownConfig[] {
  const allPatterns = [...defaultPatterns, ...customPatterns]
  const patternExt = createPatternExtension(allPatterns)
  const inlineMathParser = createInlineMathParser()

  const merged: MarkdownConfig = {
    defineNodes: [
      ...(patternExt.defineNodes ?? []),
      { name: "InlineMath", block: false },
    ],
    parseInline: [...(patternExt.parseInline ?? []), inlineMathParser],
  }
  return [merged, TaskBlock, ...createComarkExtensions()]
}
