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

///|
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(all) struct TableRow {
  cells : Array[Array[Inline]]
} derive(Eq, Debug)

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

///|
/// 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.char('\\')
  .skip(@talc.char('\n'))
  .attempt()
  .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(')'))

///|
fn link() -> InlineParser {
  @talc.char('[')
  .then(delim(']').then(inline()).some())
  .skip(@talc.char(']'))
  .bind(text => link_url.map(url => Link(text, url)))
  .attempt()
}

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

///|
fn footnote_ref() -> InlineParser {
  @talc.char('[')
  .then(@talc.char('^'))
  .then(@talc.many_chars1(@talc.none_of("]")))
  .skip(@talc.char(']'))
  .attempt()
  .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(),
      link(),
      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))
}

///|
/// 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.bind(_ => {
      @talc.Parser::new(input => {
        @talc.many_chars(@talc.not_newline)
        .run(input)
        .bind(pair => {
          let (text, rest) = pair
          line_end
          .run(rest)
          .bind(pair2 => {
            let (_, after_nl) = pair2
            parse_inlines(text).map(inlines => {
              (Heading(hashes.length(), inlines), after_nl)
            })
          })
        })
      })
    })
  })

///|
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)

///|
fn blockquote() -> BlockParser {
  quoted_line
  .some()
  .bind(lines => {
    @talc.Parser::new(input => {
      block()
      .some()
      .parse(@talc.Input::new(lines.join("")))
      .map(blocks => (Blockquote(blocks), input))
    })
  })
}

///|
/// 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()
    }
    @talc.Parser::new(input => {
      parse_inlines(text).map(inlines => (Paragraph(inlines), input))
    })
  })

///|
/// Table
let table_cell : @talc.StringParser[Array[Inline]] = @talc.many_chars(
  @talc.none_of("|\n"),
).bind(text => {
  let trimmed = text.trim()
  @talc.Parser::new(input => {
    parse_inlines(trimmed.to_owned()).map(inlines => (inlines, input))
  })
})

///|
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(_ => ())

///|
pub fn table() -> BlockParser {
  @talc.Parser::new(input => {
    match table_row().run(input) {
      Ok((header, after_header)) =>
        match table_delimiter_row.run(after_header) {
          Ok((_, after_delim)) =>
            table_row()
            .many()
            .run(after_delim)
            .map(pair => {
              let (rows, rest) = pair
              (Table(header, rows), rest)
            })
          Err(_) =>
            Err(
              @talc.ParseError::message(
                after_header, "expected table delimiter row",
              ).with_commit(),
            )
        }
      Err(e) => Err(e)
    }
  })
}

///|
/// 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()
  .bind(_ => {
    task_marker()
    .attempt()
    .optional()
    .bind(checked => {
      @talc.Parser::new(input => {
        match @talc.many_chars(@talc.not_newline).run(input) {
          Ok((text, rest)) =>
            match parse_inlines(text.trim().to_owned()) {
              Ok(content) =>
                match @talc.char('\n').run(rest) {
                  Ok((_, after_nl)) =>
                    sub_list(indent + 2)
                    .many()
                    .run(after_nl)
                    .map(pair => {
                      let (children, rest2) = pair
                      ({ checked, content, children }, rest2)
                    })
                  Err(_) =>
                    sub_list(indent + 2)
                    .many()
                    .run(rest)
                    .map(pair => {
                      let (children, rest2) = pair
                      ({ checked, content, children }, rest2)
                    })
                }
              Err(e) => Err(e)
            }
          Err(e) => Err(e)
        }
      })
    })
  })
}

///|
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 => {
      @talc.Parser::new(input => {
        parse_inlines(text.trim().to_owned()).map(inlines => {
          (FootnoteDef(label, [Paragraph(inlines)]), input)
        })
      })
    })
  })
  .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())