///|
// asciidoctor-pdf's Rouge formatter (ext/rouge/formatters/prawn.rb): a
// Rouge token stream as formatted text fragments in a theme's colours.

///|
/// Rouge's line-oriented token types, whose background spans the line.
let line_oriented_tokens : Array[String] = [
  "Generic.Inserted", "Generic.Deleted", "Generic.Heading", "Generic.Subheading",
]

///|
/// The formatter of a document (asciidoctor-pdf keeps one per converter):
/// its theme, the theme's background, the line gap and the colour of
/// highlighted lines.
priv struct RougeFormatter {
  theme : @rouge.Theme
  background : String?
  line_gap : Double
  highlight_color : Color
}

///|
/// The formatter for `rouge-style` (asciidoctor-pdf's default theme when
/// unset or unknown).
fn RougeFormatter::new(style : String?) -> RougeFormatter {
  let theme = match style.bind(@rouge.Theme::find) {
    Some(t) => t
    None => @rouge.Theme::default()
  }
  let background = theme.style_for("Text").bind(s => s.bg).map(normalize_color)
  {
    theme,
    background,
    line_gap: t_num_or("code_line_gap", 0.0),
    highlight_color: t_color("code_highlight_background_color").unwrap_or(
      rgb_hex(0xFFFFCC),
    ),
  }
}

///|
/// Rouge's `normalize_color`: without the `#`, three digits doubled.
fn normalize_color(raw : String) -> String {
  let hex = if raw.has_prefix("#") { raw[1:].to_owned() } else { raw }
  if hex.length() == 3 {
    let sb = StringBuilder()
    for c in hex {
      sb.write_char(c)
      sb.write_char(c)
    }
    sb.to_string()
  } else {
    hex
  }
}

///|
/// The background of the block when the theme gives its text one
/// (`background_color`).
fn RougeFormatter::block_background(self : RougeFormatter) -> Color? {
  self.background.bind(parse_color)
}

///|
/// Rouge's `create_fragment`: `text` in the style the theme gives `tok`
/// (with `inline_block` from the caller: a token that ends a line).
fn RougeFormatter::fragment(
  self : RougeFormatter,
  base : Style,
  tok : String,
  text : String,
  inline_block? : Bool = false,
) -> Fragment {
  let mut style = base
  match self.theme.style_for(tok) {
    Some(rules) => {
      match rules.bg.map(normalize_color) {
        Some(bg) if Some(bg) != self.background =>
          match parse_color(bg) {
            Some(color) => {
              let line_oriented = line_oriented_tokens.contains(tok)
              style = {
                ..style,
                background: Some(color),
                code_background: if line_oriented || inline_block {
                  Some(Gap(self.line_gap, line_oriented))
                } else {
                  None
                },
              }
            }
            None => ()
          }
        _ => ()
      }
      match rules.fg.map(normalize_color).bind(parse_color) {
        Some(color) => style = { ..style, color, }
        None => ()
      }
      if rules.bold || rules.italic {
        style = { ..style, bold: rules.bold, italic: rules.italic, }
      }
      if rules.underline {
        style = { ..style, underline: true, }
      }
    }
    None => ()
  }
  { text, style, anchor: None, }
}

///|
/// Rouge's `stream`: the fragments of `tokens`, each line given its number
/// (`start_line`, at least 1, padded with no-break spaces to the widest)
/// when `line_numbers`, and the highlight mark when its number is in
/// `highlight_lines`; the first space of a line is guarded unless a line
/// number precedes it.
fn RougeFormatter::format(
  self : RougeFormatter,
  base : Style,
  tokens : Array[(String, String)],
  line_numbers? : Bool = false,
  start_line? : Int = 1,
  highlight_lines? : Map[Int, Bool],
) -> Array[Fragment] {
  let out : Array[Fragment] = []
  let plain = fn(text : String) -> Fragment {
    { text, style: base, anchor: None, }
  }
  if !line_numbers && highlight_lines is None {
    let mut start_of_line = true
    for token in tokens {
      let (tok, val) = token
      if val.iter().all(c => c == '\n') {
        start_of_line = true
        out.push(plain(val))
        continue
      }
      let mut val = val
      if start_of_line && val.has_prefix(" ") {
        val = "\u{a0}" + val[1:].to_owned()
      }
      if val.contains("\n ") {
        val = val.replace_all(old="\n ", new="\n\u{a0}")
      }
      let ends_line = val.has_suffix("\n")
      let fragment = if ruby_rstrip(val) == "" {
        plain(val)
      } else {
        self.fragment(base, tok, val, inline_block=ends_line)
      }
      // a fragment ending a line is an inline block (its background, if
      // any, spans the line gap)
      start_of_line = ends_line
      out.push(fragment)
    }
    return out
  }
  let highlighted = fn(n : Int) {
    highlight_lines.bind(h => h.get(n)) is Some(true)
  }
  let mut linenum = if start_line > 0 { start_line } else { 1 }
  let mut start_of_line = false
  let linenum_fragment = fn(n : Int) -> Fragment {
    let f = self.fragment(base, "Generic.Lineno", "\{n} ")
    { ..f, style: { ..f.style, linenum: true, }, }
  }
  let mark = fn() {
    line_mark(base, self.highlight_color, Gap(self.line_gap, true))
  }
  let next_line = fn(n : Int) {
    if line_numbers {
      out.push(linenum_fragment(n))
    } else {
      start_of_line = true
    }
    if highlighted(n) {
      out.push(mark())
    }
  }
  next_line(linenum)
  for token in tokens {
    let (tok, val) = token
    if val == "\n" {
      out.push(plain("\n"))
      linenum += 1
      next_line(linenum)
    } else if val.contains("\n") {
      // a token ending in a line feed is taken for a line-oriented form
      let line_oriented = val.has_suffix("\n")
      let base_fragment = self.fragment(base, tok, val)
      let lines = val.split("\n").map(l => l.to_owned()).collect()
      for idx, line in lines {
        let last = idx == lines.length() - 1
        if last && line == "" {
          break
        }
        let mut text = if last { line } else { line + "\n" }
        if start_of_line {
          if text.has_prefix(" ") {
            text = "\u{a0}" + text[1:].to_owned()
          }
          start_of_line = false
        }
        let style = if line_oriented &&
          base_fragment.style.background is Some(_) {
          {
            ..base_fragment.style,
            code_background: Some(
              match base_fragment.style.code_background {
                Some(Gap(gap, to_edge)) => Gap(gap, to_edge)
                _ => Gap(self.line_gap, false)
              },
            ),
          }
        } else {
          base_fragment.style
        }
        out.push({ text, style, anchor: None, })
        if !last {
          linenum += 1
          next_line(linenum)
        }
      }
    } else {
      let mut val = val
      if start_of_line {
        if val.has_prefix(" ") {
          val = "\u{a0}" + val[1:].to_owned()
        }
        start_of_line = false
      }
      out.push(self.fragment(base, tok, val))
    }
  }
  // the line numbers right-aligned to the widest, with no-break spaces
  let width = linenum.to_string().length()
  if line_numbers && width > 1 {
    let width = width + 1
    out.map(f => {
      if f.style.linenum && f.text.length() < width {
        { ..f, text: "\u{a0}".repeat(width - f.text.length()) + f.text, }
      } else {
        f
      }
    })
  } else {
    out
  }
}