//! Terminal renderer for the `@cmark` event stream.
//!
//! Mirrors `terminal.mbt`'s styling decisions but walks the flat
//! `Array[@cmark.Event]` produced by the pulldown-cmark port, reusing the
//! shared math and ANSI infrastructure of this package.

///|
priv struct CommonMarkOptions {
  line_style : LineStyle
  ansi : Bool
  disable_math : Bool
  halfwidth_punct : Bool
}

///|
priv struct ListContext {
  start : Int64?
  index : Int
}

///|
/// Renders a cmark event stream as terminal text.
pub fn render_terminal_events(
  events : Array[@cmark.Event],
  line_style? : LineStyle = Unicode,
  ansi? : Bool = true,
  disable_math? : Bool = false,
  halfwidth_punct? : Bool = false,
) -> String {
  let opts = CommonMarkOptions::{
    line_style,
    ansi,
    disable_math,
    halfwidth_punct,
  }
  let out = Array::new()
  let mut i = 0
  while i < events.length() {
    match events[i] {
      Start(tag) => {
        let (s, next) = cm_render_block(
          events,
          i,
          tag,
          { start: None, index: 0 },
          opts,
        )
        if !s.is_empty() {
          out.push(s)
        }
        i = next
      }
      Rule => {
        out.push(
          term_style(
            "─".repeat(32),
            ansi=opts.ansi,
            modifier=@chalk.Modifier::Dim,
          ),
        )
        i += 1
      }
      DisplayMath(latex) => {
        out.push(cm_render_math_display(latex, opts))
        i += 1
      }
      _ => i += 1
    }
  }
  out.join("\n\n")
}

///|
fn cm_render_block(
  events : Array[@cmark.Event],
  start_ix : Int,
  tag : @cmark.Tag,
  list_ctx : ListContext,
  opts : CommonMarkOptions,
) -> (String, Int) {
  match tag {
    Table(alignments) => cm_render_table(events, start_ix, alignments, opts)
    List(start) => cm_render_list(events, start_ix, start, opts)
    _ => cm_render_generic_block(events, start_ix, tag, list_ctx, opts)
  }
}

///|
fn cm_render_generic_block(
  events : Array[@cmark.Event],
  start_ix : Int,
  tag : @cmark.Tag,
  list_ctx : ListContext,
  opts : CommonMarkOptions,
) -> (String, Int) {
  let (content, blocks, task, next) = cm_collect(
    events,
    start_ix,
    opts,
    cm_is_raw_text_block(tag),
  )
  (cm_assemble_block(tag, content, blocks, list_ctx, task, opts), next)
}

///|
fn cm_is_raw_text_block(tag : @cmark.Tag) -> Bool {
  match tag {
    CodeBlock(_) | HtmlBlock | MetadataBlock(_) => true
    _ => false
  }
}

///|
fn cm_collect(
  events : Array[@cmark.Event],
  start_ix : Int,
  opts : CommonMarkOptions,
  raw_text : Bool,
) -> (Array[String], Array[String], Bool?, Int) {
  let content = Array::new()
  let blocks = Array::new()
  let mut task : Bool? = None
  let mut last_char : Char? = None
  let mut i = start_ix + 1
  while i < events.length() {
    match events[i] {
      End(_) => break
      Start(child) => {
        let is_inline = cm_is_inline_tag(child)
        let (s, next) = if is_inline {
          cm_render_inline(events, i, child, opts, prev=last_char)
        } else {
          cm_render_block(events, i, child, { start: None, index: 0 }, opts)
        }
        if is_inline {
          content.push(s)
          last_char = cm_event_last_char(events, i)
        } else {
          blocks.push(s)
          last_char = None
        }
        i = next
      }
      TaskListMarker(checked) => {
        task = Some(checked)
        last_char = None
        i += 1
      }
      Text(s) => {
        let next_c = if i + 1 < events.length() {
          cm_event_first_char(events, i + 1)
        } else {
          None
        }
        content.push(
          if raw_text {
            s
          } else {
            cm_halfwidth_text(s, opts, prev=last_char, next=next_c)
          },
        )
        last_char = if raw_text { None } else { s.iter().last() }
        i += 1
      }
      Code(s) => {
        content.push(
          term_style(
            s,
            ansi=opts.ansi,
            background=@chalk.BackgroundColor::BgWhiteBright,
          ),
        )
        last_char = None
        i += 1
      }
      InlineMath(latex) => {
        content.push(cm_render_math_inline(latex, opts))
        last_char = None
        i += 1
      }
      DisplayMath(latex) => {
        blocks.push(cm_render_math_display(latex, opts))
        last_char = None
        i += 1
      }
      Html(s) => {
        content.push(s)
        last_char = None
        i += 1
      }
      InlineHtml(s) => {
        content.push(s)
        last_char = None
        i += 1
      }
      FootnoteReference(s) => {
        content.push(
          term_style("[" + s + "]", ansi=opts.ansi, color=@chalk.Color::Yellow),
        )
        last_char = None
        i += 1
      }
      SoftBreak => {
        content.push("\n")
        last_char = None
        i += 1
      }
      HardBreak => {
        content.push("\n")
        last_char = None
        i += 1
      }
      Rule => {
        blocks.push(
          term_style(
            "─".repeat(32),
            ansi=opts.ansi,
            modifier=@chalk.Modifier::Dim,
          ),
        )
        last_char = None
        i += 1
      }
    }
  }
  (content, blocks, task, i + 1)
}

///|
fn cm_assemble_block(
  tag : @cmark.Tag,
  content : Array[String],
  blocks : Array[String],
  list_ctx : ListContext,
  task : Bool?,
  opts : CommonMarkOptions,
) -> String {
  match tag {
    Paragraph => cm_join_content_blocks(content, blocks)
    Heading(level~, ..) =>
      term_style(
        content.join(""),
        ansi=opts.ansi,
        color=cm_heading_color(level),
        modifier=@chalk.Modifier::Bold,
        modifier2=@chalk.Modifier::Underline,
      )
    BlockQuote(_) => cm_render_blockquote(content, blocks, opts)
    CodeBlock(_) =>
      term_style(
        content.join("").trim_end().to_owned(),
        ansi=opts.ansi,
        background=@chalk.BackgroundColor::BgWhiteBright,
        modifier=@chalk.Modifier::Dim,
      )
    HtmlBlock => content.join("")
    Item => cm_assemble_list_item(content, blocks, list_ctx, task, opts)
    FootnoteDefinition(label) =>
      "[" + label + "]: " + cm_join_content_blocks(content, blocks)
    DefinitionList => cm_join_content_blocks(content, blocks)
    DefinitionListTitle =>
      term_style(
        content.join(""),
        ansi=opts.ansi,
        modifier=@chalk.Modifier::Bold,
      )
    DefinitionListDefinition(_) => cm_join_content_blocks(content, blocks)
    MetadataBlock(_) => content.join("")
    ContainerBlock(_, desc) => {
      let body = cm_join_content_blocks(content, blocks)
      guard !desc.is_empty() else { body }
      term_style(desc, ansi=opts.ansi, modifier=@chalk.Modifier::Dim) +
      "\n\n" +
      body
    }
    _ => content.join("")
  }
}

///|
fn cm_heading_color(level : @cmark.HeadingLevel) -> @chalk.Color {
  match level {
    H1 => @chalk.Color::Red
    H2 => @chalk.Color::Yellow
    H3 => @chalk.Color::Green
    _ => @chalk.Color::White
  }
}

///|
fn cm_render_blockquote(
  content : Array[String],
  blocks : Array[String],
  opts : CommonMarkOptions,
) -> String {
  let base = cm_join_content_blocks(content, blocks)
  let joined = base
    .split("\n")
    .map(fn(line) { "│ " + line.to_owned() })
    .join("\n")
  term_style(
    joined,
    ansi=opts.ansi,
    color=@chalk.Color::WhiteBright,
    modifier=@chalk.Modifier::Italic,
  )
}

///|
fn cm_join_content_blocks(
  content : Array[String],
  blocks : Array[String],
) -> String {
  let inline = content.join("")
  [..if !inline.is_empty() { [inline] }, ..blocks].join("\n\n")
}

///|
fn cm_assemble_list_item(
  content : Array[String],
  blocks : Array[String],
  list_ctx : ListContext,
  task : Bool?,
  opts : CommonMarkOptions,
) -> String {
  let inline = content.join("")
  let (first, children) = match blocks {
    [b, .. rest] if inline.is_empty() => (b, rest)
    _ => (inline, blocks[:])
  }
  let marker = cm_item_marker(list_ctx, task, opts.ansi)
  let sb = StringBuilder()
  sb.write_string(marker + " " + first)
  if !children.is_empty() {
    let indented = children
      .join("\n\n")
      .split("\n")
      .map(fn(line) { "  " + line.to_owned() })
      .join("\n")
    sb.write_string("\n")
    sb.write_string(indented)
  }
  sb.to_string()
}

///|
fn cm_item_marker(list_ctx : ListContext, task : Bool?, ansi : Bool) -> String {
  match task {
    Some(true) =>
      term_style(
        "✔",
        ansi~,
        color=@chalk.Color::Green,
        modifier=@chalk.Modifier::Bold,
      )
    Some(false) => term_style("○", ansi~, color=@chalk.Color::WhiteBright)
    None =>
      match list_ctx.start {
        Some(n) => (n + list_ctx.index.to_int64()).to_string() + "."
        None => "•"
      }
  }
}

///|
fn cm_render_list(
  events : Array[@cmark.Event],
  start_ix : Int,
  start : Int64?,
  opts : CommonMarkOptions,
) -> (String, Int) {
  let items = Array::new()
  let mut i = start_ix + 1
  while i < events.length() {
    match events[i] {
      Start(Item) => {
        let (s, next) = cm_render_block(
          events,
          i,
          @cmark.Tag::Item,
          { start, index: items.length() },
          opts,
        )
        if !s.is_empty() {
          items.push(s)
        }
        i = next
      }
      End(_) => break
      _ => i += 1
    }
  }
  (items.join("\n"), i + 1)
}

///|
fn cm_render_table(
  events : Array[@cmark.Event],
  start_ix : Int,
  alignments : Array[@cmark.Alignment],
  opts : CommonMarkOptions,
) -> (String, Int) {
  let mut header : Array[String] = Array::new()
  let rows : Array[Array[String]] = Array::new()
  let mut i = start_ix + 1
  while i < events.length() {
    match events[i] {
      Start(TableHead) | Start(TableRow) => {
        let (cells, next) = cm_render_table_row(events, i, opts)
        if header.is_empty() {
          header = cells
        } else {
          rows.push(cells)
        }
        i = next
      }
      End(_) => break
      _ => i += 1
    }
  }
  (cm_render_table_str(header, rows, alignments, opts), i + 1)
}

///|
fn cm_render_table_row(
  events : Array[@cmark.Event],
  start_ix : Int,
  opts : CommonMarkOptions,
) -> (Array[String], Int) {
  let cells = Array::new()
  let mut cell : Array[String] = Array::new()
  let mut last_char : Char? = None
  let mut i = start_ix + 1
  while i < events.length() {
    match events[i] {
      Start(TableCell) => i += 1
      End(TableCell) => {
        cells.push(cell.join(""))
        cell = Array::new()
        last_char = None
        i += 1
      }
      End(_) => break
      Start(child) => {
        let (s, next) = cm_render_inline(events, i, child, opts, prev=last_char)
        cell.push(s)
        last_char = cm_event_last_char(events, i)
        i = next
      }
      Text(s) => {
        let next_c = if i + 1 < events.length() {
          cm_event_first_char(events, i + 1)
        } else {
          None
        }
        cell.push(cm_halfwidth_text(s, opts, prev=last_char, next=next_c))
        last_char = s.iter().last()
        i += 1
      }
      _ => {
        cell.push(cm_render_inline_leaf(events[i], opts))
        last_char = None
        i += 1
      }
    }
  }
  (cells, i + 1)
}

///|
fn cm_render_table_str(
  header : Array[String],
  rows : Array[Array[String]],
  alignments : Array[@cmark.Alignment],
  opts : CommonMarkOptions,
) -> String {
  let col_count = header.length()
  let widths = Array::make(col_count, 0)
  for i in 0.. widths[i] {
        widths[i] = row[i].length()
      }
    }
  }
  let h_bar = line_style_h_bar(opts.line_style)
  let cross = line_style_cross(opts.line_style)
  let v_bar = line_style_v_bar(opts.line_style)
  let header_row = cm_render_table_row_str(
    header, widths, alignments, v_bar, opts, true,
  )
  let separator = widths.map(fn(w) { h_bar.repeat(w) }).join(cross)
  let body_rows = rows
    .map(fn(r) {
      cm_render_table_row_str(r, widths, alignments, v_bar, opts, false)
    })
    .join("\n")
  [header_row, separator, body_rows].join("\n")
}

///|
fn cm_render_table_row_str(
  cells : Array[String],
  widths : Array[Int],
  alignments : Array[@cmark.Alignment],
  v_bar : String,
  opts : CommonMarkOptions,
  is_header : Bool,
) -> String {
  let parts = Array::new()
  for i in 0.. String {
  let pad = width - cell.length()
  match alignment {
    Center => {
      let left = pad / 2
      let right = pad - left
      " ".repeat(left) + cell + " ".repeat(right)
    }
    Right => " ".repeat(pad) + cell
    _ => cell + " ".repeat(pad)
  }
}

///|
fn cm_is_inline_tag(tag : @cmark.Tag) -> Bool {
  match tag {
    Emphasis
    | Strong
    | Strikethrough
    | Highlight
    | Superscript
    | Subscript
    | Link(_)
    | Image(_) => true
    _ => false
  }
}

///|
fn cm_render_inline(
  events : Array[@cmark.Event],
  start_ix : Int,
  tag : @cmark.Tag,
  opts : CommonMarkOptions,
  prev~ : Char? = None,
) -> (String, Int) {
  let content = Array::new()
  let mut last_char : Char? = prev
  let mut i = start_ix + 1
  while i < events.length() {
    match events[i] {
      End(_) => break
      Start(child) => {
        let (s, next) = cm_render_inline(events, i, child, opts, prev=last_char)
        content.push(s)
        last_char = cm_event_last_char(events, i)
        i = next
      }
      Text(s) => {
        let next_c = if i + 1 < events.length() {
          cm_event_first_char(events, i + 1)
        } else {
          None
        }
        content.push(cm_halfwidth_text(s, opts, prev=last_char, next=next_c))
        last_char = s.iter().last()
        i += 1
      }
      _ => {
        content.push(cm_render_inline_leaf(events[i], opts))
        last_char = None
        i += 1
      }
    }
  }
  (cm_apply_inline_style(tag, content.join(""), opts), i + 1)
}

///|
/// Index of the event right after the `End` matching the `Start` at `start_ix`.
fn cm_skip_subtree(events : Array[@cmark.Event], start_ix : Int) -> Int {
  let mut depth = 0
  let mut i = start_ix
  while i < events.length() {
    match events[i] {
      Start(_) => depth += 1
      End(_) => {
        depth -= 1
        if depth == 0 {
          return i + 1
        }
      }
      _ => ()
    }
    i += 1
  }
  i
}

///|
/// The first raw text character inside the subtree starting at `start_ix`.
fn cm_subtree_first_char(events : Array[@cmark.Event], start_ix : Int) -> Char? {
  let mut i = start_ix + 1
  while i < events.length() {
    match events[i] {
      End(_) => return None
      Text(s) => return s.iter().next()
      Start(_) => {
        match cm_subtree_first_char(events, i) {
          Some(c) => return Some(c)
          None => {
            i = cm_skip_subtree(events, i)
            continue
          }
        }
      }
      _ => i += 1
    }
  }
  None
}

///|
/// The last raw text character inside the subtree starting at `start_ix`.
fn cm_subtree_last_char(events : Array[@cmark.Event], start_ix : Int) -> Char? {
  let mut last : Char? = None
  let mut i = start_ix + 1
  while i < events.length() {
    match events[i] {
      End(_) => return last
      Text(s) => {
        last = s.iter().last()
        i += 1
      }
      Start(_) => {
        match cm_subtree_last_char(events, i) {
          Some(c) => last = Some(c)
          None => ()
        }
        i = cm_skip_subtree(events, i)
      }
      _ => i += 1
    }
  }
  last
}

///|
/// The first raw text character of the inline content starting at event `ix`.
fn cm_event_first_char(events : Array[@cmark.Event], ix : Int) -> Char? {
  if ix >= events.length() {
    return None
  }
  match events[ix] {
    Text(s) => s.iter().next()
    Start(_) => cm_subtree_first_char(events, ix)
    _ => None
  }
}

///|
/// The last raw text character of the inline content starting at event `ix`.
fn cm_event_last_char(events : Array[@cmark.Event], ix : Int) -> Char? {
  if ix >= events.length() {
    return None
  }
  match events[ix] {
    Text(s) => s.iter().last()
    Start(_) => cm_subtree_last_char(events, ix)
    _ => None
  }
}

///|
fn cm_render_inline_leaf(ev : @cmark.Event, opts : CommonMarkOptions) -> String {
  match ev {
    Text(s) => cm_halfwidth_text(s, opts)
    Code(s) =>
      term_style(
        s,
        ansi=opts.ansi,
        background=@chalk.BackgroundColor::BgWhiteBright,
      )
    InlineMath(latex) => cm_render_math_inline(latex, opts)
    InlineHtml(s) => s
    Html(s) => s
    FootnoteReference(s) =>
      term_style("[" + s + "]", ansi=opts.ansi, color=@chalk.Color::Yellow)
    SoftBreak => "\n"
    HardBreak => "\n"
    _ => ""
  }
}

///|
fn cm_halfwidth_text(
  s : String,
  opts : CommonMarkOptions,
  prev~ : Char? = None,
  next~ : Char? = None,
) -> String {
  guard !opts.halfwidth_punct else { to_halfwidth_punct(s, prev~, next~) }
  s
}

///|
fn cm_apply_inline_style(
  tag : @cmark.Tag,
  text : String,
  opts : CommonMarkOptions,
) -> String {
  match tag {
    Emphasis =>
      term_style(text, ansi=opts.ansi, modifier=@chalk.Modifier::Italic)
    Strong => term_style(text, ansi=opts.ansi, modifier=@chalk.Modifier::Bold)
    Strikethrough =>
      term_style(text, ansi=opts.ansi, modifier=@chalk.Modifier::Strikethrough)
    Highlight => "==" + text + "=="
    Superscript => "^{" + text + "}"
    Subscript => "_{" + text + "}"
    Link(link_type~, dest_url~, ..) =>
      match link_type {
        Autolink | Email => text
        _ => "[" + text + "](" + dest_url + ")"
      }
    Image(dest_url~, ..) =>
      term_style(
        "[img: " + text + "](" + dest_url + ")",
        ansi=opts.ansi,
        color=@chalk.Color::Yellow,
        modifier=@chalk.Modifier::Italic,
      )
    _ => text
  }
}

///|
fn cm_render_math_inline(latex : String, opts : CommonMarkOptions) -> String {
  guard !opts.disable_math else { "$" + latex + "$" }
  render_math(latex, false, target=Unicode)
}

///|
fn cm_render_math_display(latex : String, opts : CommonMarkOptions) -> String {
  guard !opts.disable_math else { "$$" + latex + "$$" }
  render_math(latex, true, target=Unicode)
}