///|
fn Ctx::expose_doc(
  self : Ctx,
  path : @syntax.NodePath,
  x : @ast.Node[@ast.TopLevelExpose],
) -> @pretty.Doc raise PrintError {
  match x.value {
    InfixExpose(symbol) =>
      @pretty.text("(" + self.operator_symbol(path, symbol) + ")")
    FunctionExpose(name) => self.lower_name(path, name)
    TypeOrAliasExpose(name) => upper_name(path, name)
    TypeExpose(t) =>
      upper_name(path, t.name) +
      (if t.open is Some(_) {
        @pretty.text("(") + self.inner_alone(x.range) + @pretty.text("..)")
      } else {
        @pretty.empty()
      })
  }
}

///|
/// The comments of an explicit exposing list as elm-format keeps them:
/// before and after each item (`C2`), by the `expose_order` key of the
/// item. The comments of items with the same key (duplicates, or the same
/// item in two imports of one module) are joined.
priv struct ExposeComments {
  /// The comments between `exposing` and `(`.
  before_paren : Array[@ast.Node[String]]
  items : Map[
    (Int, String),
    (Array[@ast.Node[String]], Array[@ast.Node[String]]),
  ]
}

///|
fn ExposeComments::new() -> ExposeComments {
  { before_paren: [], items: Map([]), }
}

///|
/// Takes the comments of the items of exposing list `e` into `into`: for
/// each item, the comments after the `(` or `,` before it, and the
/// comments after it up to the next `,` or the `)`. Call it with the list
/// of the source, before normalization, so that a comment goes with the
/// item it is next to.
fn Ctx::take_expose_comments(
  self : Ctx,
  e : @ast.Node[@ast.Exposing],
  into : ExposeComments,
) -> Unit {
  guard e.value is Explicit(items) && items.length() > 0 else { return }
  let paren = self.token_before(items[0].range.start)
  into.before_paren.append(
    self.take_if(Leading, items[0].range, c => ends_before(c, paren)),
  )
  for i, x in items {
    // The comments after the `,` that trail the item before it.
    let pre = if i > 0 { self.take(Trailing, items[i - 1].range) } else { [] }
    pre.append(self.take(Leading, x.range))
    let post = match items.get(i + 1) {
      Some(next) => {
        let comma = self.token_before(next.range.start)
        let p = self.take_if(Trailing, x.range, c => ends_before(c, comma))
        p.append(self.take_if(Leading, next.range, c => ends_before(c, comma)))
        p
      }
      None => [..self.take(Trailing, x.range), ..self.take(Inner, e.range)]
    }
    let key = expose_order(x.value)
    match into.items.get(key) {
      Some((a, b)) => {
        a.append(pre)
        b.append(post)
      }
      None => into.items[key] = (pre, post)
    }
  }
}

///|
/// `Split` when the source list `e` has a line break inside its
/// parentheses (elm-format Parse/Module.hs `listing`, `trackNewline`).
fn Ctx::list_split(self : Ctx, e : @ast.Node[@ast.Exposing]) -> Split {
  match e.value {
    Explicit(items) =>
      match items.get(0).bind(x => self.token_before(x.range.start)) {
        Some(paren) => self.split_within({ start: paren, end: e.range.end, })
        None => self.join()
      }
    All(_) => self.join()
  }
}

///|
/// `( a\n, b\n)`: the parts each on its own line, then the comments
/// `footer` (elm-format `group'` with its `extraFooter`).
fn stacked_list(
  parts : Array[@pretty.Doc],
  footer : Array[@ast.Node[String]],
) -> @pretty.Doc {
  let mut d = @pretty.text("( ") + @pretty.nest(2, parts[0])
  for i in 1.. (Array[@ast.Node[String]], @pretty.Doc, Bool) raise PrintError {
  match e.value {
    All(_) => {
      // `(..)` has no children: its comments before the `(` are inner too.
      let paren = self.token_back_to(e.range.end, "(", 3)
      let before_paren = self.take_if(Inner, e.range, c => ends_before(c, paren))
      (
        before_paren,
        @pretty.text("(") + self.inner_alone(e.range) + @pretty.text("..)"),
        false,
      )
    }
    Explicit(items) => {
      guard !items.is_empty() else {
        raise PrintError(path~, problem=EmptyExposing)
      }
      let position : Map[(Int, String), Int] = Map([])
      for i, x in items {
        position[expose_order(x.value)] = i
      }
      let item_doc = (x : @ast.Node[@ast.TopLevelExpose]) => {
        let key = expose_order(x.value)
        self.expose_doc(
          path.child("explicit", position.get(key).unwrap_or(0)),
          x,
        )
      }
      let none = ([], [])
      let documented : Map[(Int, String), Unit] = Map([])
      for k in 0..= groups.documented {
              let (a, b) = comments.items
                .get(expose_order(x.value))
                .unwrap_or(none)
              pre.append(a)
              post.append(b)
            }
          }
          parts.push(
            commented(pre, @pretty.join(names, @pretty.text(", ")), post).0,
          )
        }
      }
      // The comments of items that normalization removed.
      footer.append(self.take_within(e.range))
      let fixed = groups.documented > 0 && split is Fit
      let split = if fixed { Join } else { split }
      let list = match split {
        _ if !flat || !footer.is_empty() => stacked_list(parts, footer)
        // The enclosing group breaks the list.
        Fit => {
          let mut body = @pretty.nest(2, parts[0])
          for i in 1..
          @pretty.text("(") +
          @pretty.join(parts, @pretty.text(", ")) +
          @pretty.text(")")
        Split => stacked_list(parts, footer)
      }
      (comments.before_paren, list, fixed && flat && footer.is_empty())
    }
  }
}

///|
/// The comments before the `(` of exposing list `e`, after `exposing` on
/// its line. After a line comment, the list of `(..)` goes on the next
/// line.
fn before_paren_doc(
  e : @ast.Node[@ast.Exposing],
  cs : Array[@ast.Node[String]],
) -> @pretty.Doc {
  let gap = if e.value is All(_) && cs.iter().any(is_line_comment) {
    @pretty.nest(4, @pretty.hardline())
  } else {
    @pretty.empty()
  }
  comments_after(cs) + gap
}

///|
/// ` exposing (a, b)`, or ` exposing\n    ( a\n    , b\n    )` after a
/// module header. After an import (`import_ = true`), the broken form puts
/// `exposing` on its own line, as elm-format 0.8.7 does:
/// `\n    exposing\n        ( a\n        , b\n        )`. `previous` is
/// the range of the name before it. `groups`, `comments` and `split` are
/// as for `Ctx::exposing_list`.
fn Ctx::exposing_doc(
  self : Ctx,
  path : @syntax.NodePath,
  e : @ast.Node[@ast.Exposing],
  previous : @ast.Range,
  groups : ExposingGroups,
  comments : ExposeComments,
  split : Split,
  import_? : Bool = false,
) -> @pretty.Doc raise PrintError {
  // The comments before `exposing` go on lines of their own.
  let cs = self.take(Leading, e.range)
  let lead = if cs.is_empty() {
    None
  } else {
    Some(@pretty.nest(4, comment_block(cs, before=true, after=true)))
  }
  let (paren_comments, items, list_one_line) = self.exposing_list(
    path, e, groups, comments, split,
  )
  // elm-format Box.hs formatModuleLine and formatImport: `exposing` with
  // the comments after it (`formatCommented`) is multi-line when a comment
  // is, and then the list goes on the next line (`spaceSepOrIndented`):
  // in the column of `exposing` after a module header, one tab stop right
  // of it after an import.
  if !paren_comments.iter().all(one_line) {
    let rest = @pretty.hardline() +
      comment_block(paren_comments) +
      @pretty.hardline() +
      items
    return lead.unwrap_or(@pretty.nest(4, @pretty.hardline())) +
      @pretty.nest(
        4,
        @pretty.text("exposing") +
        (if import_ { @pretty.nest(4, rest) } else { rest }),
      )
  }
  let before_paren = before_paren_doc(e, paren_comments)
  match e.value {
    All(_) =>
      lead.unwrap_or(self.after(previous, " ")) +
      @pretty.text("exposing") +
      before_paren +
      @pretty.text(" ") +
      items
    Explicit(_) => {
      // A list that is one line follows `exposing` on its line, unless a
      // line comment breaks the header.
      let gap = if list_one_line &&
        lead is None &&
        !self.line_comment_after(previous) &&
        !comments.before_paren.iter().any(is_line_comment) {
        @pretty.text(" ")
      } else {
        @pretty.line()
      }
      let list = @pretty.text("exposing") +
        before_paren +
        @pretty.nest(4, gap + items)
      match (lead, import_) {
        (Some(l), true) => l + @pretty.group(@pretty.nest(4, list))
        (Some(l), false) => l + @pretty.group(list)
        (None, true) =>
          if self.line_comment_after(previous) {
            // Nothing may follow the line comment on its line.
            @pretty.nest(4, @pretty.hardline()) +
            @pretty.group(@pretty.nest(4, list))
          } else {
            @pretty.group(@pretty.nest(4, @pretty.line() + list))
          }
        (None, false) => @pretty.group(self.after(previous, " ") + list)
      }
    }
  }
}

///|
/// `command = MyCmd` in the `where` record of an effect module, with the
/// comments `before` it (after its `{` or `,`) and `after` its name (before
/// the next `,` or `}`). A comment after the name that needs its own line
/// puts the name and its comments on lines of their own (elm-format). Also
/// gives whether a comment of the field ends its line. `label_at` is the
/// start of the label (`command`), the text after the comments before it.
fn effect_field(
  path : @syntax.NodePath,
  label : String,
  label_at : @ast.Location,
  name : @ast.Node[String],
  before : Array[@ast.Node[String]],
  after : Array[@ast.Node[String]],
) -> (@pretty.Doc, Bool) raise PrintError {
  guard is_upper(name.value) else {
    raise PrintError(path~, problem=InvalidName(Upper, name.value))
  }
  let own_lines = after.iter().any(c => off_line(c, name.range))
  let value = if own_lines {
    @pretty.tab(
      4,
      @pretty.hardline() +
      @pretty.text(name.value) +
      comment_block(after, before=true),
    )
  } else {
    @pretty.text(" " + name.value) + comments_after(after)
  }
  (
    comments_before(before, label_at) + @pretty.text(label + " =") + value,
    own_lines || comments_break(before, label_at),
  )
}

///|
/// The module header. `groups` and `comments` are the groups and the
/// comments of its exposing list (see `Ctx::exposing_list`).
fn Ctx::module_doc(
  self : Ctx,
  path : @syntax.NodePath,
  m : @ast.Node[@ast.Module],
  groups : ExposingGroups,
  comments : ExposeComments,
) -> @pretty.Doc raise PrintError {
  match m.value {
    NormalModule(d) | PortModule(d) => {
      let keyword = if m.value is PortModule(_) {
        "port module"
      } else {
        "module"
      }
      let name = d.module_name
      @pretty.text(keyword) +
      self.space_before(None, name.range, 4).0 +
      self.with_comments(
        name.range,
        @pretty.text(module_name(path.child("moduleName", 0), name.value)),
      ) +
      self.exposing_doc(
        path.child("exposingList", 0),
        d.exposing_list,
        name.range,
        groups,
        comments,
        self.join(),
      )
    }
    // `effect module A where { command = C } exposing (a)` on one line. A
    // comment that ends its line in the header puts `where`, the record and
    // `exposing` with its list each on lines of their own, indented by 4
    // (elm-format).
    EffectModule(d) => {
      let name = d.module_name
      let head = @pretty.text("effect module") +
        self.space_before(None, name.range, 4).0 +
        @pretty.text(module_name(path.child("moduleName", 0), name.value))
      let names = []
      if d.command is Some(c) {
        names.push(("command", c))
      }
      if d.subscription is Some(s) {
        names.push(("subscription", s))
      }
      guard names.get(0) is Some((_, first)) else {
        raise PrintError(path~, problem=NoFields)
      }
      // The comments after the name: before `where` they stay with the
      // name; between `where` and `{` they go before `where`; after `{`
      // they go before the first field.
      let where_at = self.token_after(name.range.end)
      let brace = self.token_back_to(first.range.start, "{", 3)
      let after_name = []
      let before_where = []
      let carried = []
      for c in self.take(Trailing, name.range) {
        if ends_before(c, where_at) {
          after_name.push(c)
        } else if ends_before(c, brace) {
          before_where.push(c)
        } else {
          carried.push(c)
        }
      }
      before_where.append(
        self.take_if(Leading, first.range, c => ends_before(c, brace)),
      )
      // Each field gets the comments after its `{` or `,`, and the comments
      // after its name up to the next `,` or `}`; the comments after the
      // `}` go before `exposing`.
      let e = d.exposing_list
      let fields = []
      let mut breaks = false
      let mut before = [..carried, ..self.take(Leading, first.range)]
      let mut before_exposing = []
      for k, x in names {
        let (label, n) = x
        let (after, moved) = self.trailing_split(n.range)
        let next_before = match names.get(k + 1) {
          Some((_, m)) => {
            let comma = self.token_back_to(m.range.start, ",", 3)
            after.append(
              self.take_if(Leading, m.range, c => ends_before(c, comma)),
            )
            [..moved, ..self.take(Leading, m.range)]
          }
          None => {
            let close = self.token_before(e.range.start)
            after.append(
              self.take_if(Leading, e.range, c => ends_before(c, close)),
            )
            before_exposing = [..moved, ..self.take(Leading, e.range)]
            []
          }
        }
        let label_at = self
          .token_back_to(n.range.start, label, 2)
          .unwrap_or(n.range.start)
        let (field, field_breaks) = effect_field(
          path.child(label, 0),
          label,
          label_at,
          n,
          before,
          after,
        )
        fields.push(field)
        breaks = breaks || field_breaks
        before = next_before
      }
      let name_breaks = after_name.iter().any(is_line_comment)
      if !name_breaks &&
        before_where.is_empty() &&
        !breaks &&
        before_exposing.is_empty() {
        let (_, last) = names[names.length() - 1]
        return head +
          comments_after(after_name) +
          @pretty.text(" where { ") +
          @pretty.join(fields, @pretty.text(", ")) +
          @pretty.text(" }") +
          self.exposing_doc(
            path.child("exposingList", 0),
            e,
            last.range,
            groups,
            comments,
            self.join(),
          )
      }
      let name_comments = if name_breaks {
        comment_block(after_name, before=true)
      } else {
        comments_after(after_name)
      }
      // The record follows `where` on its line unless it breaks, or a
      // comment comes before `where`.
      let record = sequence(self.join(), "{", "}", fields)
      let where_clause = if name_breaks || !before_where.is_empty() {
        @pretty.text("where") + @pretty.nest(4, @pretty.hardline() + record)
      } else {
        @pretty.text("where") +
        @pretty.group(@pretty.nest(4, @pretty.line() + record))
      }
      let (paren_comments, list, _) = self.exposing_list(
        path.child("exposingList", 0),
        e,
        groups,
        comments,
        self.join(),
      )
      let before_paren = before_paren_doc(e, paren_comments)
      // elm-format `spaceSepOrIndented`: the list goes on the next line when
      // the header up to `exposing` or the list is multi-line.
      head +
      @pretty.group(
        @pretty.group(
          @pretty.nest(
            4,
            name_comments +
            comment_block(before_where, before=true) +
            @pretty.line() +
            where_clause +
            comment_block(before_exposing, before=true) +
            @pretty.line() +
            @pretty.text("exposing") +
            before_paren,
          ),
        ) +
        @pretty.nest(4, @pretty.line() + @pretty.group(list)),
      )
    }
  }
}

///|
/// An import. `comments` and `split` are as for `Ctx::exposing_list`.
/// `after_name` are comments to print after the module name: those after
/// the names of the imports that this one merges (elm-format keeps them
/// with the clause after them).
fn Ctx::import_doc(
  self : Ctx,
  path : @syntax.NodePath,
  i : @ast.Node[@ast.Import],
  comments : ExposeComments,
  split : Split,
  after_name? : Array[@ast.Node[String]] = [],
) -> @pretty.Doc raise PrintError {
  let name = i.value.module_name
  // The comments after `as` (taken before the name's own comments).
  let moved = if i.value.module_alias is Some(_) {
    self.after_token(name.range)
  } else {
    []
  }
  let mut d = @pretty.text("import") +
    self.space_before(None, name.range, 4).0 +
    self.with_comments(
      name.range,
      @pretty.text(module_name(path.child("moduleName", 0), name.value)),
    ) +
    comments_after(after_name)
  let mut previous = name.range
  if i.value.module_alias is Some(a) {
    // An alias is one upper-case name: `import Html.Events as E`.
    let p = path.child("moduleAlias", 0)
    let alias_name = module_name(p, a.value)
    guard a.value.length() == 1 else {
      raise PrintError(path=p, problem=InvalidName(Upper, alias_name))
    }
    d = d +
      self.separator(previous, " as", moved) +
      self.space_before(None, a.range, 4).0 +
      self.with_comments(a.range, @pretty.text(alias_name))
    previous = a.range
  }
  if i.value.exposing_list is Some(x) {
    let groups = match x.value {
      Explicit(items) => ExposingGroups::single(items)
      All(_) => { groups: [], documented: 0, }
    }
    d = d +
      self.exposing_doc(
        path.child("exposingList", 0),
        x,
        previous,
        groups,
        comments,
        split,
        import_=true,
      )
  }
  d
}