///|
/// AST types
pub(all) enum Inline {
  Plain(String)
  Strong(Array[Inline])
  Emph(Array[Inline])
  Code(String)
  Image(String, String)
  SoftBreak
  HardBreak
  InlineMath(String)
  FootnoteRef(String)
} derive(Eq, Debug)

///|
pub extend Inline with Eq::{not_equal, equal}

///|
pub extend Inline with Debug::{to_repr}

///|
pub(all) enum Block {
  Heading(Int, Array[Inline])
  Paragraph(Array[Inline])
  Blockquote(Array[Block])
  UnorderedList(Array[MdListItem])
  OrderedList(Array[MdListItem])
  FencedCode(String?, String)
  DisplayMath(String)
  ThematicBreak
  Table(TableRow, Array[TableRow])
  FootnoteDef(String, Array[Block])
} derive(Eq, Debug)

///|
pub extend Block with Eq::{not_equal, equal}

///|
pub extend Block with Debug::{to_repr}

///|
pub(all) struct TableRow {
  cells : Array[Array[Inline]]
} derive(Eq, Debug)

///|
pub extend TableRow with Eq::{not_equal, equal}

///|
pub extend TableRow with Debug::{to_repr}

///|
pub(all) struct MdListItem {
  checked : Bool?
  content : Array[Inline]
  children : Array[Block]
} derive(Eq, Debug)

///|
pub extend MdListItem with Eq::{not_equal, equal}

///|
pub extend MdListItem with Debug::{to_repr}

///|
/// Inline parsers
pub let special_chars = "*_`[\\$"

///|
pub fn delim(c : Char) -> @talc.StringParser[Unit] {
  @talc.char(c).not_followed_by("'\{c}'")
}

///|
pub type InlineParser = @talc.StringParser[Inline]

///|
pub type BlockParser = @talc.StringParser[Block]

///|
let hard_break : InlineParser = @talc.string("\\\n").map(_ => HardBreak)

///|
let soft_break : InlineParser = @talc.char('\n').map(_ => SoftBreak)

///|
let escaped : InlineParser = @talc.char('\\')
  .then(@talc.one_of(special_chars))
  .map(c => Plain(c.to_string()))

///|
let code_span : InlineParser = @talc.char('`')
  .then(@talc.many_chars(@talc.none_of("`")))
  .skip(@talc.char('`'))
  .map(c => Code(c))

///|
let inline_math : InlineParser = @talc.char('$')
  .then(@talc.many_chars(@talc.none_of("$")))
  .skip(@talc.char('$'))
  .attempt()
  .map(c => InlineMath(c))

///|
let link_url : @talc.SParser = @talc.char('(')
  .then(@talc.many_chars(@talc.none_of(")")))
  .skip(@talc.char(')'))

///|
let image : InlineParser = @talc.string("![")
  .then(@talc.many_chars(@talc.none_of("]")))
  .skip(@talc.char(']'))
  .bind(alt => link_url.map(url => Image(alt, url)))

///|
fn footnote_ref() -> InlineParser {
  @talc.string("[^")
  .then(@talc.many_chars1(@talc.none_of("]")))
  .skip(@talc.char(']'))
  .map(label => FootnoteRef(label))
}

///|
fn strong() -> InlineParser {
  @talc.char('*')
  .then(delim('*').then(inline()).some())
  .skip(@talc.char('*'))
  .map(c => Strong(c))
}

///|
fn emph() -> InlineParser {
  @talc.char('_')
  .then(delim('_').then(inline()).some())
  .skip(@talc.char('_'))
  .map(c => Emph(c))
}

///|
let plain_char : InlineParser = {
  let non_special = @talc.none_of(special_chars + "\n")
  non_special.bind(c => {
    @talc.many_chars(non_special).map(rest => Plain("\{c}\{rest}"))
  })
}

///|
let plain_bracket : InlineParser = @talc.one_of("[]").map(c => {
  Plain(c.to_string())
})

///|
fn inline() -> InlineParser {
  @talc.delay(() => {
    @talc.choice([
      hard_break,
      escaped,
      image,
      footnote_ref(),
      strong(),
      emph(),
      inline_math,
      code_span,
      soft_break,
      plain_char,
      plain_bracket,
    ])
  })
}

///|
pub fn parse_inlines(text : String) -> Result[Array[Inline], @talc.ParseError] {
  inline().many().parse(@talc.Input::new(text))
}

///|
/// Parses `text` as inline markdown, as a parser that consumes no input.
///
/// Lets the inline parser be composed in a talc chain, e.g.
/// `rest_of_line.bind(text => inlines(text.trim().to_owned()))`.
fn inlines(text : String) -> @talc.StringParser[Array[Inline]] {
  @talc.Parser::new(input => {
    parse_inlines(text).map(inlines => (inlines, input))
  })
}

///|
/// Block parsers
let line_end : @talc.StringParser[Unit] = @talc.char('\n')
  .map(_ => ())
  .or(@talc.eof())

///|
let blank_line : @talc.StringParser[Unit] = {
  let ws_no_nl = @talc.satisfy("non-newline whitespace", c => {
    Char::is_whitespace(c) && c != '\n'
  })
  @talc.many_chars(ws_no_nl).skip(@talc.char('\n')).map(_ => ()).attempt()
}

///|
let thematic_break : BlockParser = @talc.string("---")
  .then(@talc.char('-').many())
  .skip(line_end)
  .map(_ => ThematicBreak)

///|
let heading : BlockParser = @talc.char('#')
  .some()
  .bind(hashes => {
    @talc.space.then(
      @talc.line_content
      .bind(text => line_end.then(inlines(text)))
      .map(inlines => Heading(hashes.length(), inlines)),
    )
  })

///|
let rest_of_line : @talc.SParser = @talc.many_chars1(@talc.not_newline)
  .bind(content => line_end.map(_ => content + "\n"))
  .or(@talc.char('\n').map(_ => "\n"))

///|
let fenced_code : BlockParser = {
  let not_nl = @talc.not_newline
  @talc.string("```")
  .then(@talc.many_chars1(not_nl).optional())
  .skip(line_end)
  .bind(lang => {
    let terminator = @talc.string("```").skip(line_end)
    rest_of_line
    .many_until(terminator)
    .map(lines => FencedCode(lang, lines.join("")))
  })
}

///|
/// Quote
let quoted_line : @talc.SParser = @talc.char('>')
  .then(@talc.space.optional())
  .then(rest_of_line)

///|
/// Parses `text` as a block document, as a parser that consumes no input.
///
/// The parallel of [`inlines`](`inlines`) for block content; used by
/// `blockquote` to re-parse the joined quoted lines as a fresh document.
/// (Can't reuse `document` itself: that would be a definition cycle.)
fn blocks_of(text : String) -> @talc.StringParser[Array[Block]] {
  @talc.Parser::new(input => {
    blank_line
    .many()
    .then(blocks())
    .skip(blank_line.many())
    .skip(@talc.eof())
    .run(@talc.Input::new(text))
    .map(pair => {
      let (blocks, _) = pair
      (blocks, input)
    })
  })
}

///|
fn blockquote() -> BlockParser {
  quoted_line
  .some()
  .bind(lines => blocks_of(lines.join("")).map(blocks => Blockquote(blocks)))
}

///|
/// Paragraph
let paragraph_line : @talc.SParser = blank_line
  .not_followed_by("blank line")
  .then(delim('#'))
  .then(@talc.string("---").not_followed_by("\"---\""))
  .then(@talc.string("```").not_followed_by("\"```\""))
  .then(@talc.string("$$").not_followed_by("\"$$\""))
  .then(delim('>'))
  .then(@talc.char('-').then(@talc.space).not_followed_by("'- '"))
  .then(@talc.char('+').then(@talc.space).not_followed_by("'+ '"))
  .then(delim('|'))
  .then(rest_of_line)

///|
let paragraph : BlockParser = paragraph_line
  .some()
  .bind(lines => {
    let joined = lines.join("")
    let text = match () {
      _ if joined.has_suffix("\\\n") => joined.trim_start().to_owned()
      _ => joined.trim().to_owned()
    }
    inlines(text).map(inlines => Paragraph(inlines))
  })

///|
/// Table
let table_cell : @talc.StringParser[Array[Inline]] = @talc.many_chars(
  @talc.none_of("|\n"),
).bind(text => inlines(text.trim().to_owned()))

///|
fn table_row() -> @talc.StringParser[TableRow] {
  @talc.char('|')
  .then(table_cell)
  .bind(first => {
    @talc.char('|')
    .then(line_end.not_followed_by("trailing pipe"))
    .then(table_cell)
    .attempt()
    .many()
    .map(rest => {
      let all = Array::new(capacity=rest.length() + 1)
      all.push(first)
      all.append(rest)
      TableRow::{ cells: all }
    })
  })
  .skip(@talc.char('|').optional())
  .skip(line_end)
}

///|
let table_delimiter_cell : @talc.StringParser[Unit] = @talc.space
  .many()
  .then(@talc.char(':').optional())
  .then(@talc.char('-').some())
  .then(@talc.char(':').optional())
  .skip(@talc.space.many())
  .map(_ => ())

///|
let table_delimiter_row : @talc.StringParser[Unit] = @talc.char('|')
  .then(table_delimiter_cell)
  .then(@talc.char('|').then(table_delimiter_cell).attempt().many())
  .skip(@talc.char('|').optional())
  .skip(line_end)
  .map(_ => ())

///|
/// A table delimiter row, failing with a clear message at the row start
/// when the header row is not followed by `| --- | --- |`-style markers.
let table_delimiter : @talc.StringParser[Unit] = @talc.Parser::new(input => {
  table_delimiter_row
  .run(input)
  .map_err(_ => {
    @talc.ParseError::message(input, "expected table delimiter row").with_commit()
  })
})

///|
pub fn table() -> BlockParser {
  table_row().bind(header => {
    table_delimiter.then(table_row().many()).map(rows => Table(header, rows))
  })
}

///|
/// Lists
fn task_marker() -> @talc.StringParser[Bool] {
  @talc.choice([
    @talc.string("[ ]").map(_ => false),
    @talc.string("[x]").map(_ => true),
  ])
}

///|
fn list_item(marker : Char, indent : Int) -> @talc.StringParser[MdListItem] {
  let istr = String::repeat(" ", indent)
  @talc.string(istr)
  .then(@talc.char(marker))
  .skip(@talc.space)
  .attempt()
  .then(task_marker().attempt().optional())
  .bind(checked => list_item_body(checked, indent))
}

///|
/// Parses the text of a list item and its sub-lists.
fn list_item_body(
  checked : Bool?,
  indent : Int,
) -> @talc.StringParser[MdListItem] {
  @talc.line_content
  .bind(text => inlines(text.trim().to_owned()))
  .skip(@talc.char('\n').optional())
  .bind(content => {
    sub_list(indent + 2).many().map(children => { checked, content, children })
  })
}

///|
fn unordered_list_at(indent : Int) -> BlockParser {
  list_item('-', indent).some().map(items => UnorderedList(items))
}

///|
fn ordered_list_at(indent : Int) -> BlockParser {
  list_item('+', indent).some().map(items => OrderedList(items))
}

///|
fn sub_list(indent : Int) -> BlockParser {
  @talc.choice([unordered_list_at(indent), ordered_list_at(indent)])
}

///|
let unordered_list : BlockParser = unordered_list_at(0)

///|
let ordered_list : BlockParser = ordered_list_at(0)

///|
/// Footnote definition
let footnote_def : BlockParser = @talc.char('[')
  .then(@talc.char('^'))
  .then(@talc.many_chars1(@talc.none_of("]")))
  .skip(@talc.string("]:"))
  .skip(@talc.space)
  .bind(label => {
    rest_of_line.bind(text => {
      inlines(text.trim().to_owned()).map(inlines => {
        FootnoteDef(label, [Paragraph(inlines)])
      })
    })
  })
  .attempt()

///|
/// Display math block
let display_math_block : BlockParser = @talc.string("$$")
  .bind(_ => {
    @talc.choice([
      @talc.char('\n').bind(_ => {
        let terminator = @talc.string("$$").skip(@talc.char('\n').optional())
        rest_of_line
        .many_until(terminator)
        .map(lines => DisplayMath(lines.join("").trim_end().to_owned()))
      }),
      @talc.many_chars(@talc.none_of("$"))
      .skip(@talc.string("$$"))
      .skip(@talc.char('\n').optional())
      .map(content => DisplayMath(content.trim().to_owned())),
    ])
  })
  .attempt()

///|
/// Document
fn block() -> BlockParser {
  @talc.choice([
    thematic_break,
    heading,
    fenced_code,
    display_math_block,
    blockquote(),
    footnote_def,
    table(),
    unordered_list,
    ordered_list,
    paragraph,
  ])
}

///|
fn blocks() -> @talc.StringParser[Array[Block]] {
  block()
  .bind(head => {
    blank_line
    .many()
    .then(block())
    .attempt()
    .many()
    .map(tail => Array::new(capacity=tail.length() + 1)..append([head, ..tail]))
  })
  .or(@talc.pure([]))
}

///|
pub let document : @talc.StringParser[Array[Block]] = blank_line
  .many()
  .then(blocks())
  .skip(blank_line.many())
  .skip(@talc.eof())