///|
/// The text of a comment as elm-format writes it: line ends as `\n`, no
/// trailing spaces.
fn comment_text(c : String) -> String {
  c.replace_all(old="\r\n", new="\n").trim_end().to_owned()
}

///|
/// Haskell `isSpace` (GHC): the Latin-1 spaces and the Unicode category
/// `Zs`.
fn is_haskell_space(c : Char) -> Bool {
  match c {
    ' ' | '\t' | '\n' | '\r' | '\u{0B}' | '\u{0C}' | '\u{A0}' | '\u{1680}' =>
      true
    '\u{2000}'..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' => true
    _ => false
  }
}

///|
/// The lines of a comment as elm-format writes them. A line comment is one
/// line. A block comment follows elm-format 0.8.7: `{--}` and `{--...-}`
/// (comment tricks) lose only the spaces after `{--` and before `-}`
/// (Parse/Whitespace.hs `multiComment`, Box.hs `CommentTrickBlock`). Any
/// other block comment loses the spaces after `{-` and before `-}`, and the
/// lines after the first lose their common indentation (`trimIndent`).
/// Then (Box.hs `formatComment`) no line gives `{- -}`; one line `l` gives
/// `{- l -}`; more lines give `{- ` and the first line, each other line
/// after 3 spaces, and `-}` on a line of its own. No line ends with a
/// space.
fn comment_lines(c : String) -> Array[String] {
  let text = comment_text(c)
  guard text.has_prefix("{-") && text.has_suffix("-}") && text.length() >= 4 else {
    return [text]
  }
  guard text != "{--}" else { return [text] }
  let body = text.view(start_offset=2, end_offset=text.length() - 2)
  if body.has_prefix("-") {
    let b = body.view(start_offset=1).trim_start(chars=" ").trim_end(chars=" ")
    return ("{--" + b.to_owned() + "-}")
      .split("\n")
      .map(l => l.to_owned())
      .collect()
  }
  let b = body.trim_start(chars=" ").trim_end(chars=" ").to_owned()
  // Haskell `lines`: no last empty line when the text ends with a line
  // feed.
  let lines : Array[String] = b.split("\n").map(l => l.to_owned()).collect()
  if b == "" || b.has_suffix("\n") {
    lines.pop() |> ignore
  }
  let mut depth : Int? = None
  for i in 1.. if n < d { n } else { d }
          None => n
        },
      )
    }
  }
  let d = depth.unwrap_or(0)
  for i in 1.. ["{- -}"]
    1 => ["{- " + lines[0] + " -}"]
    _ => {
      let out = ["{- " + lines[0]]
      for i in 1.. l.trim_end().to_owned())
    }
  }
}

///|
/// A comment as elm-format writes it (see `comment_lines`): the lines after
/// the first start in the column of the comment.
fn comment_doc(c : @ast.Node[String]) -> @pretty.Doc {
  let lines = comment_lines(c.value)
  if c.value.has_prefix("{--") {
    // elm-format writes a comment trick as one literal: the lines after
    // the first get no indentation.
    @pretty.verbatim(lines.join("\n"))
  } else if lines.length() == 1 {
    @pretty.text(lines[0])
  } else {
    @pretty.align(@pretty.join(lines.map(@pretty.text), @pretty.hardline()))
  }
}

///|
fn is_line_comment(c : @ast.Node[String]) -> Bool {
  c.value.has_prefix("--")
}

///|
/// The not yet printed comments with this role at this range for which
/// `keep` is true, in source order. They are marked printed.
fn Ctx::take_if(
  self : Ctx,
  role : Role,
  r : @ast.Range,
  keep : (@ast.Node[String]) -> Bool,
) -> Array[@ast.Node[String]] {
  guard self.comments is Some(t) else { return [] }
  let out = []
  for i in t.matching(role, r) {
    let c = t.placed[i].comment
    if keep(c) {
      t.printed[i] = true
      out.push(c)
    }
  }
  out
}

///|
fn Ctx::take(
  self : Ctx,
  role : Role,
  r : @ast.Range,
) -> Array[@ast.Node[String]] {
  self.take_if(role, r, _ => true)
}

///|
/// Whether comment `c` ends its line: it is a line comment, or the text
/// after it starts at `next` on a later row.
fn ends_line(c : @ast.Node[String], next : @ast.Location) -> Bool {
  is_line_comment(c) || next.row > c.range.end.row
}

///|
/// Whether comment `c`, after the node at `r`, needs a line before it or
/// after it: it is a line comment, or it starts on a later row than the
/// node ends.
fn off_line(c : @ast.Node[String], r : @ast.Range) -> Bool {
  is_line_comment(c) || c.range.start.row > r.end.row
}

///|
/// Comments each on its own line, joined by line breaks. With `before`, a
/// line break comes first; with `after`, one comes last. Nothing when there
/// are no comments.
fn comment_block(
  cs : Array[@ast.Node[String]],
  before? : Bool = false,
  after? : Bool = false,
) -> @pretty.Doc {
  guard !cs.is_empty() else { return @pretty.empty() }
  (if before { @pretty.hardline() } else { @pretty.empty() }) +
  @pretty.join(cs.map(comment_doc), @pretty.hardline()) +
  (if after { @pretty.hardline() } else { @pretty.empty() })
}

///|
/// Whether `comments_before(cs, next)` has a line break.
fn comments_break(cs : Array[@ast.Node[String]], next : @ast.Location) -> Bool {
  for i, c in cs {
    let after = if i + 1 < cs.length() { cs[i + 1].range.start } else { next }
    if ends_line(c, after) {
      return true
    }
  }
  false
}

///|
/// Comments before the text that starts at `next`. A line comment ends its
/// line. A block comment ends its line when the source has a line break
/// after it; else one space follows it.
fn comments_before(
  cs : Array[@ast.Node[String]],
  next : @ast.Location,
) -> @pretty.Doc {
  let mut d = @pretty.empty()
  for i, c in cs {
    let after = if i + 1 < cs.length() { cs[i + 1].range.start } else { next }
    d = d +
      comment_doc(c) +
      (if ends_line(c, after) { @pretty.hardline() } else { @pretty.text(" ") })
  }
  d
}

///|
/// Comments before the text after them, joined as elm-format joins them
/// (`spaceSepOrStack`): a block comment is followed by `gap`, a line
/// comment by a line break. With `gap = line()` in a group that holds the
/// text, the block comments go on lines of their own when the text is
/// multi-line.
fn comments_joined(
  cs : Array[@ast.Node[String]],
  gap : @pretty.Doc,
) -> @pretty.Doc {
  let mut d = @pretty.empty()
  for c in cs {
    d = d +
      comment_doc(c) +
      (if is_line_comment(c) { @pretty.hardline() } else { gap })
  }
  d
}

///|
/// Comments after a node on its last line: ` {- a -} -- b`.
fn comments_after(cs : Array[@ast.Node[String]]) -> @pretty.Doc {
  let mut d = @pretty.empty()
  for c in cs {
    d = d + @pretty.text(" ") + comment_doc(c)
    if is_line_comment(c) {
      // Nothing may follow a line comment on its line. `if_break(b, f)`
      // has a hard line when `f` has one, so this forces the enclosing
      // group to break and prints nothing in the broken form.
      d = d + @pretty.if_break(@pretty.empty(), @pretty.hardline())
    }
  }
  d
}

///|
/// The leading comments of the node with range `r`; marks them printed.
fn Ctx::leading(self : Ctx, r : @ast.Range) -> @pretty.Doc {
  comments_before(self.take(Leading, r), r.start)
}

///|
/// The trailing comments of the node with range `r`; marks them printed.
fn Ctx::trailing(self : Ctx, r : @ast.Range) -> @pretty.Doc {
  comments_after(self.take(Trailing, r))
}

///|
/// The inner comments of the node with range `owner`, for the place after
/// its last child and before its closing token: a blank line, then each
/// comment on its own line (elm-format, for a list). The container breaks
/// the line before its closing token. Empty when there are none.
///
/// `last` is the range of the last child when elm-syntax's range of that
/// child runs to the closing token (a record field or setter): its inner
/// comments come first.
fn Ctx::inner(
  self : Ctx,
  owner : @ast.Range,
  last? : @ast.Range? = None,
) -> @pretty.Doc {
  let cs = match last {
    Some(r) => self.take(Inner, r)
    None => []
  }
  cs.append(self.take(Inner, owner))
  guard !cs.is_empty() else { return @pretty.empty() }
  @pretty.hardline() + comment_block(cs, before=true)
}

///|
/// The inner comments of the node with range `owner`, for the place after
/// its last child and before its closing token, with no blank line: each
/// comment on its own line (elm-format, for parentheses and tuples). The
/// container breaks the line before its closing token. Empty when there
/// are none.
fn Ctx::inner_close(self : Ctx, owner : @ast.Range) -> @pretty.Doc {
  let cs = self.take(Inner, owner)
  guard !cs.is_empty() else { return @pretty.empty() }
  @pretty.hardline() + comment_block(cs)
}

///|
/// The inner comments of the node with range `owner` when it has no
/// children, between its opening and closing tokens: `[{- a -}]`,
/// `[-- a\n]`. After a `{` (`brace = true`), a line comment gets a space
/// first: `{--` would start a block comment.
fn Ctx::inner_alone(
  self : Ctx,
  owner : @ast.Range,
  brace? : Bool = false,
) -> @pretty.Doc {
  let cs = self.take(Inner, owner)
  let mut d = match cs.get(0) {
    Some(c) if brace && is_line_comment(c) => @pretty.text(" ")
    _ => @pretty.empty()
  }
  for i, c in cs {
    d = d + comment_doc(c)
    match cs.get(i + 1) {
      Some(next) =>
        d = d +
          (if ends_line(c, next.range.start) {
            @pretty.hardline()
          } else {
            @pretty.text(" ")
          })
      None => if is_line_comment(c) { d = d + @pretty.hardline() }
    }
  }
  d
}

///|
/// `d` with the leading and trailing comments of its node.
fn Ctx::with_comments(
  self : Ctx,
  r : @ast.Range,
  d : @pretty.Doc,
) -> @pretty.Doc {
  self.leading(r) + d + self.trailing(r)
}

///|
/// The range of the first regular comment that is not printed.
fn CommentTable::unprinted(self : CommentTable) -> @ast.Range? {
  for i, p in self.placed {
    if !self.printed[i] {
      return Some(p.comment.range)
    }
  }
  None
}

///|
/// Whether a line comment trails the node at `r`, printed or not.
fn Ctx::line_comment_after(self : Ctx, r : @ast.Range) -> Bool {
  guard self.comments is Some(t) else { return false }
  match t.index.get(index_key(Trailing, r)) {
    Some(list) => list.iter().any(i => is_line_comment(t.placed[i].comment))
    None => false
  }
}

///|
/// `text(s)` after the node at `r`. When a line comment trails that node,
/// nothing may follow on its line, so the leading spaces of `s` become a
/// line break, indented by 4 (a continuation line).
fn Ctx::after(self : Ctx, r : @ast.Range, s : String) -> @pretty.Doc {
  if self.line_comment_after(r) {
    @pretty.nest(4, @pretty.hardline()) +
    @pretty.text(s.trim_start().to_owned())
  } else {
    @pretty.text(s)
  }
}

///|
/// `Ctx::inner` for a container with `count` children, `Ctx::inner_alone`
/// for one with none.
fn Ctx::inner_for(self : Ctx, owner : @ast.Range, count : Int) -> @pretty.Doc {
  if count == 0 {
    self.inner_alone(owner)
  } else {
    self.inner(owner)
  }
}

///|
/// The start of the token `n` tokens before the token at `loc`, when there
/// is source.
fn Ctx::token_back(self : Ctx, loc : @ast.Location, n : Int) -> @ast.Location? {
  guard self.source is Some(facts) else { return None }
  let i = facts.first_at_or_after(loc)
  if i >= n {
    Some(facts.starts[i - n])
  } else {
    None
  }
}

///|
/// The start of the token before the token at `loc`, when there is source.
fn Ctx::token_before(self : Ctx, loc : @ast.Location) -> @ast.Location? {
  self.token_back(loc, 1)
}

///|
/// Whether comment `c` ends before the token at `loc` (none when there is
/// no source).
fn ends_before(c : @ast.Node[String], loc : @ast.Location?) -> Bool {
  match loc {
    Some(l) => at_or_before(c.range.end, l)
    None => false
  }
}

///|
/// The comments before item `r` of a sequence that has items before it, for
/// the place before its `,`: a blank line, then each comment on its own
/// line (elm-format). Comments after the `,` stay with the item.
/// `previous` is the range of the item before it when elm-syntax's range of
/// that item runs to the `,` (a record field or setter): its inner comments
/// come first. Empty when there are none.
fn Ctx::before_item(
  self : Ctx,
  previous : @ast.Range?,
  r : @ast.Range,
) -> @pretty.Doc {
  let comma = self.token_before(r.start)
  let cs = match previous {
    Some(p) => self.take(Inner, p)
    None => []
  }
  cs.append(self.take_if(Leading, r, c => ends_before(c, comma)))
  comment_block(cs, before=true, after=true)
}

///|
/// The trailing comments of record field or setter `r` that come before the
/// `,` of the next item `next`. elm-syntax's range of a field can run to
/// that `,`; then the comments after the `,` trail the field, and
/// `Ctx::after_comma` takes them.
fn Ctx::trailing_before_comma(
  self : Ctx,
  r : @ast.Range,
  next : @ast.Range?,
) -> @pretty.Doc {
  match next {
    None => self.trailing(r)
    Some(n) => {
      let comma = self.token_before(n.start)
      comments_after(
        self.take_if(Trailing, r, c => {
          match comma {
            Some(l) => compare_location(c.range.start, l) < 0
            None => true
          }
        }),
      )
    }
  }
}

///|
/// The comments after the `,` before record field or setter `r`: the rest
/// of the trailing comments of the field before it (see
/// `Ctx::trailing_before_comma`), then the leading comments of `r`.
fn Ctx::after_comma(
  self : Ctx,
  previous : @ast.Range?,
  r : @ast.Range,
) -> @pretty.Doc {
  let cs = match previous {
    Some(p) => self.take(Trailing, p)
    None => []
  }
  cs.append(self.take(Leading, r))
  comments_before(cs, r.start)
}

///|
/// Whether a not yet printed comment with this role at this range makes
/// `keep` true.
fn Ctx::has_comment(
  self : Ctx,
  role : Role,
  r : @ast.Range,
  keep : (@ast.Node[String]) -> Bool,
) -> Bool {
  guard self.comments is Some(t) else { return false }
  t.matching(role, r).iter().any(i => keep(t.placed[i].comment))
}

///|
/// The comments before item `r` of a `let` or `case`: the trailing comments
/// of the item before it (at `previous`), which elm-format moves here, on
/// lines of their own, then the leading comments of `r`.
fn Ctx::moved_then_leading(
  self : Ctx,
  previous : @ast.Range?,
  r : @ast.Range,
) -> @pretty.Doc {
  let moved = match previous {
    Some(p) => self.take(Trailing, p)
    None => []
  }
  comment_block(moved, after=true) + self.leading(r)
}

///|
/// The start of the nearest token `lexeme` at most `n` tokens before the
/// token at `loc`, when there is source.
fn Ctx::token_back_to(
  self : Ctx,
  loc : @ast.Location,
  lexeme : String,
  n : Int,
) -> @ast.Location? {
  guard self.source is Some(facts) else { return None }
  let i = facts.first_at_or_after(loc)
  for k in 1..<=n {
    if i - k >= 0 && facts.lexeme_at(facts.starts[i - k]) == Some(lexeme) {
      return Some(facts.starts[i - k])
    }
  }
  None
}

///|
/// The start of the first token at or after `loc`, when there is source.
fn Ctx::token_after(self : Ctx, loc : @ast.Location) -> @ast.Location? {
  guard self.source is Some(facts) else { return None }
  facts.starts.get(facts.first_at_or_after(loc))
}

///|
/// The trailing comments of the node at `r` that come before the next
/// token, then those after it (marks both printed).
fn Ctx::trailing_split(
  self : Ctx,
  r : @ast.Range,
) -> (Array[@ast.Node[String]], Array[@ast.Node[String]]) {
  let next = self.token_after(r.end)
  let before = self.take_if(Trailing, r, c => {
    next is None || ends_before(c, next)
  })
  (before, self.take(Trailing, r))
}

///|
/// The block comments that trail the node at `r` but come after the next
/// token, a separator such as `:`, `=`, `->` or `as` (elm-format keeps
/// them after it: `x {- a -} : {- b -} Int`). Take them before the node's
/// own trailing comments, and print them after the separator with
/// `Ctx::separator`. Line comments stay before the separator (see
/// `Ctx::after`).
fn Ctx::after_token(self : Ctx, r : @ast.Range) -> Array[@ast.Node[String]] {
  guard self.token_after(r.end) is Some(next) else { return [] }
  self.take_if(Trailing, r, c => {
    !is_line_comment(c) && !ends_before(c, Some(next))
  })
}

///|
/// The separator `s` (such as ` :`) after the node at `r` (see
/// `Ctx::after`), then the comments `moved` after it (see
/// `Ctx::after_token`).
fn Ctx::separator(
  self : Ctx,
  r : @ast.Range,
  s : String,
  moved : Array[@ast.Node[String]],
) -> @pretty.Doc {
  self.after(r, s) + comments_after(moved)
}

///|
/// The not yet printed comments inside `r`, in source order; marks them
/// printed. For the comments of nodes that `normalize_file` removes (a
/// duplicate exposed item, the alias of a merged import).
fn Ctx::take_within(self : Ctx, r : @ast.Range) -> Array[@ast.Node[String]] {
  guard self.comments is Some(t) else { return [] }
  let out = []
  for i, p in t.placed {
    let c = p.comment
    if !t.printed[i] &&
      at_or_before(r.start, c.range.start) &&
      at_or_before(c.range.end, r.end) {
      t.printed[i] = true
      out.push(c)
    }
  }
  out
}

///|
/// Whether comment `c` prints on one line (a block comment with no line
/// break).
fn one_line(c : @ast.Node[String]) -> Bool {
  !is_line_comment(c) && comment_lines(c.value).length() == 1
}

///|
/// Comments as elm-format joins them (`formatComments`): on one line when
/// each of them is one line, else each on its own line.
fn comment_box(cs : Array[@ast.Node[String]]) -> @pretty.Doc {
  let gap = if cs.iter().all(one_line) {
    @pretty.text(" ")
  } else {
    @pretty.hardline()
  }
  @pretty.join(cs.map(comment_doc), gap)
}

///|
/// `d` with the comments `pre` before it and `post` after it (elm-format
/// `formatCommented`): on one line when each comment is one line, else the
/// comments before, `d` and the comments after on lines of their own. Also
/// gives whether it is one line.
fn commented(
  pre : Array[@ast.Node[String]],
  d : @pretty.Doc,
  post : Array[@ast.Node[String]],
) -> (@pretty.Doc, Bool) {
  let flat = pre.iter().all(one_line) && post.iter().all(one_line)
  let parts = []
  if !pre.is_empty() {
    parts.push(comment_box(pre))
  }
  parts.push(d)
  if !post.is_empty() {
    parts.push(comment_box(post))
  }
  let gap = if flat { @pretty.text(" ") } else { @pretty.hardline() }
  (@pretty.join(parts, gap), flat)
}

///|
/// Whether a comment is inside the parentheses at `parens` but outside the
/// expression at `inner` in them. elm-format keeps such parentheses.
fn Ctx::comment_in_parens(
  self : Ctx,
  parens : @ast.Range,
  inner : @ast.Range,
) -> Bool {
  guard self.comments is Some(t) else { return false }
  t.has_between(parens.start, inner.start) ||
  t.has_between(inner.end, parens.end)
}