///|
fn can_have_multiple_per_line(c : @ast.Constant) -> Bool {
  match c {
    Int(..) | Float(..) | String(..) | Var(..) => true
    Tuple(..)
    | Todo(..)
    | List(..)
    | Record(..)
    | RecordUpdate(..)
    | BitArray(..)
    | BinaryOperator(..) => false
  }
}

///|
fn is_constant_breakable_argument(c : @ast.Constant) -> Bool {
  match c {
    Tuple(..) | List(..) | BitArray(..) => true
    _ => false
  }
}

///|
fn is_tuple(c : @ast.Constant) -> Bool {
  c is Tuple(..)
}

///|
fn is_binop(c : @ast.Constant) -> Bool {
  c is BinaryOperator(..)
}

///|
fn bin_op_name(c : @ast.Constant) -> @ast.BinOp? {
  match c {
    BinaryOperator(operator~, ..) => Some(operator)
    _ => None
  }
}

///|
fn bin_op_precedence(c : @ast.Constant) -> Int32 {
  match c {
    BinaryOperator(operator~, ..) => precedence(operator)
    _ => max_precedence
  }
}

///|
/// The compiler's `items_sequence_packing`.
fn[A] Formatter::items_sequence_packing(
  self : Formatter,
  items : ArrayView[A],
  tail : A?,
  location_of : (A) -> @span.SrcSpan,
  can_have_multiple_per_line : (A) -> Bool,
  list_location : @span.SrcSpan,
) -> ItemsPacking {
  // The trailing comma is looked for after the tail, or after the last item
  // when there is no tail (`tail.or_else(items.last())`).
  let last_end = match tail {
    Some(tl) => Some(location_of(tl).end)
    None => items.last().map(l => location_of(l).end)
  }
  // The empty-line check ends at the last item, or at the tail when there
  // is no item (`items.last().or(tail)`).
  let last = match items.last() {
    Some(l) => Some(l)
    None => tail
  }
  let ends_with_trailing_comma = match last_end {
    Some(end) => self.has_trailing_comma(end, list_location.end)
    None => false
  }
  let all = items.to_owned()
  if tail is Some(tl) {
    all.push(tl)
  }
  let has_multiple_elements_per_line = self.has_items_on_the_same_line(
    all, location_of,
  )
  let first = if items.length() > 0 { Some(items[0]) } else { None }
  let has_empty_lines_between_elements = match (first, last) {
    (Some(first), Some(last)) =>
      self.empty_lines_at < self.empty_lines.length() &&
      self.empty_lines[self.empty_lines_at] >= location_of(first).end &&
      self.empty_lines[self.empty_lines_at] < location_of(last).start
    _ => false
  }
  if has_empty_lines_between_elements {
    BreakOnePerLine
  } else if !ends_with_trailing_comma {
    FitOnePerLine
  } else if tail is None &&
    items.iter().all(can_have_multiple_per_line) &&
    has_multiple_elements_per_line &&
    self.spans_multiple_lines(list_location.start, list_location.end) {
    FitMultiplePerLine
  } else {
    BreakOnePerLine
  }
}

///|
/// The compiler's `has_items_on_the_same_line`.
fn[A] Formatter::has_items_on_the_same_line(
  self : Formatter,
  items : Array[A],
  location_of : (A) -> @span.SrcSpan,
) -> Bool {
  let mut previous : @span.SrcSpan? = None
  for item in items {
    let loc = location_of(item)
    if previous is Some(p) && !self.spans_multiple_lines(p.end, loc.start) {
      return true
    }
    previous = Some(loc)
  }
  false
}

///|
/// The compiler's `format_call_arg`.
fn[A] Formatter::format_call_arg(
  self : Formatter,
  argument : @ast.CallArg[A],
  value_location : (A) -> @span.SrcSpan,
  format_value : (A) -> @pretty.Doc raise FormatError,
) -> @pretty.Doc raise FormatError {
  let shorthand = argument.implicit is None &&
    argument.location == value_location(argument.value)
  match (shorthand, argument.label) {
    (true, Some(label)) => {
      let comments = self.pop_comments(argument.location.start)
      commented(t(label) + t(":"), comments[:])
    }
    (_, Some(label)) => {
      let comments = self.pop_comments(argument.location.start)
      let value = format_value(argument.value)
      commented(t(label) + t(": "), comments[:]) + value
    }
    (_, None) => format_value(argument.value)
  }
}

///|
/// The compiler's `append_inlinable_wrapped_arguments`.
fn[A] Formatter::append_inlinable_wrapped_arguments(
  self : Formatter,
  doc : @pretty.Doc,
  values : Array[A],
  location : @span.SrcSpan,
  location_of : (A) -> @span.SrcSpan,
  is_breakable_argument : (A) -> Bool,
  to_doc : (A) -> @pretty.Doc raise FormatError,
) -> @pretty.Doc raise FormatError {
  match values.last() {
    Some(last) if is_breakable_argument(last) &&
      !self.any_comments(location_of(last).start) &&
      !self.any_comment_between(location_of(last).end, location.end) => {
      let docs = []
      for v in values[:values.length() - 1] {
        docs.push(to_doc(v))
      }
      docs.push(@pretty.next_break_fits(group(to_doc(last)), Enabled))
      group(
        @pretty.next_break_fits(
          doc + self.wrap_function_call_arguments(docs, location),
          Disabled,
        ),
      )
    }
    _ => {
      let docs = []
      for v in values {
        docs.push(to_doc(v))
      }
      group(doc + self.wrap_function_call_arguments(docs, location))
    }
  }
}

///|
/// The compiler's `constant_call_arg`.
fn Formatter::constant_call_arg(
  self : Formatter,
  argument : @ast.CallArg[@ast.Constant],
) -> @pretty.Doc raise FormatError {
  self.format_call_arg(argument, c => c.location(), c => self.const_expr(c))
}

///|
/// The compiler's `comma_separated_constant_item`.
fn Formatter::comma_separated_constant_item(
  self : Formatter,
  constant : @ast.Constant,
  siblings : Int32,
) -> @pretty.Doc raise FormatError {
  match constant {
    BinaryOperator(operator~, left~, right~, ..) if siblings > 1 => {
      let comments = self.pop_comments(constant.location().start)
      let doc = group(
        self.constant_binary_operator(operator, left, right, true),
      )
      commented(doc, comments[:])
    }
    _ => group(self.const_expr(constant))
  }
}

///|
/// The compiler's `append_as_message_constant`.
fn Formatter::append_as_message_constant(
  self : Formatter,
  doc : @pretty.Doc,
  message : @ast.Constant?,
) -> @pretty.Doc raise FormatError {
  guard message is Some(message) else { return doc }
  let comments = self.pop_comments(message.location().start)
  let doc = match printed_comments(comments[:], false) {
    Some(c) =>
      group(doc) +
      t(" as") +
      nest(line_doc + c + line_doc + group(self.const_expr(message)))
    None => group(doc) + t(" as ") + nest(group(self.const_expr(message)))
  }
  group(doc)
}

///|
/// The compiler's `constant_binary_operator`, with the left spine walked
/// in a loop (see `bin_op`).
fn Formatter::constant_binary_operator(
  self : Formatter,
  name : @ast.BinOp,
  left : @ast.Constant,
  right : @ast.Constant,
  nest_steps : Bool,
) -> @pretty.Doc raise FormatError {
  let ops = [name]
  let rights = [right]
  let mut leaf = left
  while leaf is BinaryOperator(operator~, left=l, right=r, ..) {
    ops.push(operator)
    rights.push(r)
    leaf = l
  }
  let n = ops.length() - 1
  let mut doc = self.constant_binary_operator_side(ops[n], leaf, nest_steps)
  for k = n; k >= 0; k = k - 1 {
    let comments = self.pop_comments(rights[k].location().start)
    let name_doc = breakable_space + commented(binop(ops[k]), comments[:])
    let right_side = self.constant_binary_operator_side(
      ops[k],
      rights[k],
      nest_steps,
    )
    doc = doc +
      (if nest_steps { nest(name_doc) } else { name_doc }) +
      t(" ") +
      right_side
    if k > 0 {
      doc = if can_be_grouped_with(ops[k], ops[k - 1]) {
        doc
      } else {
        operator_side(group(doc), precedence(ops[k - 1]), precedence(ops[k]))
      }
    }
  }
  doc
}

///|
/// The compiler's `constant_binary_operator_side`.
fn Formatter::constant_binary_operator_side(
  self : Formatter,
  operator : @ast.BinOp,
  side : @ast.Constant,
  nest_steps : Bool,
) -> @pretty.Doc raise FormatError {
  let side_doc = match side {
    String(value~, ..) => bin_op_string(value)
    BinaryOperator(operator~, left~, right~, ..) =>
      self.constant_binary_operator(operator, left, right, nest_steps)
    _ => self.const_expr(side)
  }
  match bin_op_name(side) {
    Some(side_name) if can_be_grouped_with(side_name, operator) => side_doc
    _ =>
      operator_side(
        group(side_doc),
        precedence(operator),
        bin_op_precedence(side),
      )
  }
}

///|
/// The compiler's `constant_record_update_argument`.
fn Formatter::constant_record_update_argument(
  self : Formatter,
  argument : @ast.RecordUpdateArg[@ast.Constant],
) -> @pretty.Doc raise FormatError {
  let comments = self.pop_comments(argument.location.start)
  if argument.location == argument.value.location() {
    commented(t(argument.label) + t(":"), comments[:])
  } else {
    let doc = group(
      t(argument.label) + t(": ") + self.const_expr(argument.value),
    )
    if is_binop(argument.value) {
      nest(commented(doc, comments[:]))
    } else {
      commented(doc, comments[:])
    }
  }
}

///|
/// A piece of a constant record update: the record or one argument.
priv enum ConstUpdatePiece {
  Record(@ast.RecordBeingUpdated[@ast.Constant])
  Argument(@ast.RecordUpdateArg[@ast.Constant])
}

///|
fn ConstUpdatePiece::location(self : ConstUpdatePiece) -> @span.SrcSpan {
  match self {
    Record(r) => r.location
    Argument(a) => a.location
  }
}

///|
/// The compiler's `const_record_update`.
fn Formatter::const_record_update(
  self : Formatter,
  module_ : @ast.SpannedString?,
  name : String,
  record : @ast.RecordBeingUpdated[@ast.Constant],
  arguments : ArrayView[@ast.RecordUpdateArg[@ast.Constant]],
  location : @span.SrcSpan,
) -> @pretty.Doc raise FormatError {
  let constructor_doc = match module_ {
    Some(m) => t(m.name) + t(".") + t(name)
    None => t(name)
  }
  let pieces : Array[ConstUpdatePiece] = [Record(record)]
  for a in arguments {
    pieces.push(Argument(a))
  }
  self.append_inlinable_wrapped_arguments(
    constructor_doc,
    pieces,
    location,
    p => p.location(),
    p => {
      match p {
        Argument(a) => is_constant_breakable_argument(a.value)
        Record(r) => is_constant_breakable_argument(r.base)
      }
    },
    p => {
      match p {
        Argument(a) => self.constant_record_update_argument(a)
        Record(r) => {
          let comments = self.pop_comments(r.location.start)
          commented(t("..") + self.const_expr(r.base), comments[:])
        }
      }
    },
  )
}

///|
/// The compiler's `const_list`.
fn Formatter::const_list(
  self : Formatter,
  elements : ArrayView[@ast.Constant],
  location : @span.SrcSpan,
  tail : @ast.Constant?,
) -> @pretty.Doc raise FormatError {
  if elements.is_empty() {
    let comments = self.pop_comments(location.end)
    return match printed_comments(comments[:], false) {
      None => t("[]")
      Some(c) =>
        @pretty.force_break(
          t("[") + nest(empty_break) + c + empty_break + t("]"),
        )
    }
  }
  let packing = self.items_sequence_packing(
    elements,
    tail,
    c => c.location(),
    can_have_multiple_per_line,
    location,
  )
  let comma = match packing {
    FitMultiplePerLine => flex_comma
    FitOnePerLine | BreakOnePerLine => comma_break
  }
  let list_size = elements.length() + (if tail is Some(_) { 1 } else { 0 })
  let mut is_empty = true
  let mut elements_doc = @pretty.empty()
  for element in elements {
    let empty_lines = self.pop_empty_lines(element.location().start)
    let element_doc = self.comma_separated_constant_item(element, list_size)
    elements_doc = if is_empty {
      is_empty = false
      element_doc
    } else if empty_lines {
      elements_doc + @pretty.set_nesting(0, comma) + line_doc + element_doc
    } else {
      elements_doc + comma + element_doc
    }
  }
  let elements_doc = @pretty.next_break_fits(elements_doc, Disabled)
  let doc = open_square_break + elements_doc
  let (doc, final_break) = match tail {
    None => (nest(doc), trailing_comma_break)
    Some(tl) => {
      let comments = self.pop_comments(tl.location().start)
      let tail_doc = commented(t("..") + self.const_expr(tl), comments[:])
      (nest(doc + comma_break + tail_doc), empty_break)
    }
  }
  let comments = self.pop_comments(location.end)
  let doc = match printed_comments(comments[:], false) {
    None => doc + final_break + t("]")
    Some(c) =>
      @pretty.force_break(doc + nest(final_break) + nest(c) + line_doc + t("]"))
  }
  match packing {
    FitOnePerLine | FitMultiplePerLine => group(doc)
    BreakOnePerLine => @pretty.force_break(doc)
  }
}

///|
/// The compiler's `const_tuple`.
fn Formatter::const_tuple(
  self : Formatter,
  elements : ArrayView[@ast.Constant],
  location : @span.SrcSpan,
) -> @pretty.Doc raise FormatError {
  if elements.is_empty() {
    let comments = self.pop_comments(location.end)
    return match printed_comments(comments[:], false) {
      None => t("#()")
      Some(c) =>
        @pretty.force_break(
          t("#(") + nest(empty_break) + c + empty_break + t(")"),
        )
    }
  }
  let n = elements.length()
  self.append_inlinable_wrapped_arguments(
    t("#"),
    elements.to_owned(),
    location,
    c => c.location(),
    c => !is_tuple(c) && is_constant_breakable_argument(c),
    c => self.comma_separated_constant_item(c, n),
  )
}

///|
/// The compiler's `const_expr`. One unit of fuel per nesting level.
fn Formatter::const_expr(
  self : Formatter,
  value : @ast.Constant,
) -> @pretty.Doc raise FormatError {
  self.nested(op_format, () => {
    let comments = self.pop_comments(value.location().start)
    let document = match value {
      Todo(message~, ..) => self.append_as_message_constant(t("todo"), message)
      Int(value~, ..) => int(value)
      Float(value~, ..) => float(value)
      String(value~, ..) => string(value)
      List(elements~, location~, tail~) =>
        self.const_list(elements, location, tail)
      Tuple(elements~, location~) => self.const_tuple(elements, location)
      BitArray(segments~, location~) => {
        let segment_docs = []
        for s in segments {
          segment_docs.push(bit_array_segment(s, c => self.const_expr(c)))
        }
        let packing = self.items_sequence_packing(
          segments,
          None,
          s => s.location,
          s => can_have_multiple_per_line(s.value),
          location,
        )
        self.bit_array(segment_docs, packing, location)
      }
      Record(name~, arguments~, module_~, location~, ..) => {
        let head = match module_ {
          None => t(name)
          Some(m) => t(m.name) + t(".") + t(name)
        }
        match arguments {
          None => head
          Some(arguments) => {
            let docs = []
            for a in arguments {
              docs.push(self.constant_call_arg(a))
            }
            group(head + self.wrap_arguments(docs, location.end))
          }
        }
      }
      Var(name~, module_~, ..) =>
        match module_ {
          None => t(name)
          Some(m) => t(m.name) + t(".") + t(name)
        }
      BinaryOperator(operator~, left~, right~, ..) =>
        self.constant_binary_operator(operator, left, right, false)
      RecordUpdate(module_~, name~, record~, arguments~, location~, ..) =>
        self.const_record_update(module_, name, record, arguments, location)
    }
    commented(document, comments[:])
  })
}

///|
/// The constant arm of the compiler's `definition`.
fn Formatter::module_constant(
  self : Formatter,
  c : @ast.ModuleConstant,
) -> @pretty.Doc raise FormatError {
  let attrs = attributes(c.deprecation, c.publicity, None, None)
  // The compiler counts `const ` as 5 columns (its CONST_SPACE_DOCUMENT
  // declares 5), so the space has no width here.
  let head = attrs +
    pub_(c.publicity) +
    t("const") +
    @pretty.zero_width(" ") +
    t(c.name)
  let head = match c.annotation {
    None => head
    Some(type_) => head + t(": ") + self.type_ast(type_)
  }
  let value = match c.value {
    List(..) | Tuple(..) | BitArray(..) => {
      let expression_comments = self.pop_comments(c.value.location().start)
      let expression_doc = self.const_expr(c.value)
      match printed_comments(expression_comments[:], true) {
        Some(comments) => nest(line_doc + comments + expression_doc)
        None => t(" ") + expression_doc
      }
    }
    _ => nest(breakable_space + group(self.const_expr(c.value)))
  }
  let value = group(@pretty.next_break_fits(value, Disabled))
  head + t(" =") + value
}