///|
fn Ctx::generics(
  self : Ctx,
  path : @syntax.NodePath,
  names : ArrayView[@ast.Node[String]],
) -> @pretty.Doc raise PrintError {
  let mut d = @pretty.empty()
  for i, g in names {
    d = d +
      @pretty.text(" ") +
      self.lower_name(path.child("generics", i), g.value)
  }
  d
}

///|
fn documentation_line(
  path : @syntax.NodePath,
  doc : @ast.Node[String]?,
) -> @pretty.Doc raise PrintError {
  match doc {
    Some(x) =>
      documentation_doc(path.child("documentation", 0), x.value) +
      @pretty.hardline()
    None => @pretty.empty()
  }
}

///|
fn Ctx::declaration_doc(
  self : Ctx,
  path : @syntax.NodePath,
  level : Int,
  d : @ast.Node[@ast.Declaration],
) -> @pretty.Doc raise PrintError {
  check_level(path, level)
  match d.value {
    FunctionDeclaration(f) => self.function_doc(path, level, f)
    AliasDeclaration(a) =>
      documentation_line(path, a.documentation) +
      @pretty.text("type alias ") +
      upper_name(path.child("name", 0), a.name.value) +
      self.generics(path, a.generics) +
      @pretty.text(" =") +
      @pretty.nest(
        4,
        @pretty.hardline() +
        self.type_doc(
          path.child("typeAnnotation", 0),
          level + 1,
          a.type_annotation,
          AnyType,
        ),
      )
    CustomTypeDeclaration(t) => {
      guard !t.constructors.is_empty() else {
        raise PrintError(path~, problem=NoConstructors)
      }
      let mut ctors = @pretty.empty()
      for i, c in t.constructors {
        let p = path.child("constructors", i)
        let args = []
        for j, a in c.value.arguments {
          args.push(
            self.type_doc(p.child("arguments", j), level + 1, a, TypeArg),
          )
        }
        ctors = ctors +
          @pretty.hardline() +
          @pretty.text(if i == 0 { "= " } else { "| " }) +
          spaced(upper_name(p.child("name", 0), c.value.name.value), args)
      }
      documentation_line(path, t.documentation) +
      @pretty.text("type ") +
      upper_name(path.child("name", 0), t.name.value) +
      self.generics(path, t.generics) +
      @pretty.nest(4, ctors)
    }
    // A port's `name` and `typeAnnotation` are fields of the declaration.
    PortDeclaration(s) =>
      @pretty.text("port ") + self.signature_doc(path, level, s)
    InfixDeclaration(i) => {
      // elm-format pads the direction to 5 columns: `infix left  0 (|>) = apR`.
      let direction = match i.direction.value {
        Left => "left "
        Right => "right"
        Non => "non  "
      }
      let precedence = i.precedence.value
      // The precedence is not a node of the syntax tree (`NodeRef`), so the
      // path is the declaration's.
      guard precedence >= 0 && precedence <= 9 else {
        raise PrintError(path~, problem=InvalidPrecedence(precedence))
      }
      let symbol = self.operator_symbol(
        path.child("operator", 0),
        i.operator.value,
      )
      @pretty.text(
        "infix " +
        direction +
        " " +
        precedence.to_string() +
        " (" +
        symbol +
        ") = ",
      ) +
      self.lower_name(path.child("function", 0), i.function.value)
    }
    Destructuring(pattern, value) =>
      // As in a let: a destructuring pattern is a term.
      self.pattern_doc(path.child("pattern", 0), level + 1, pattern, PatternArg) +
      @pretty.text(" =") +
      @pretty.nest(
        4,
        @pretty.hardline() +
        self.expr_doc(path.child("expression", 0), level + 1, value, AnyExpr),
      )
  }
}

///|
fn Ctx::expose_doc(
  self : Ctx,
  path : @syntax.NodePath,
  x : @ast.TopLevelExpose,
) -> @pretty.Doc raise PrintError {
  match x {
    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("(..)") } else { @pretty.empty() })
  }
}

///|
/// ` 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        )`.
fn Ctx::exposing_doc(
  self : Ctx,
  path : @syntax.NodePath,
  e : @ast.Node[@ast.Exposing],
  import_? : Bool = false,
) -> @pretty.Doc raise PrintError {
  match e.value {
    All(_) => @pretty.text(" exposing (..)")
    Explicit(items) => {
      guard !items.is_empty() else {
        raise PrintError(path~, problem=EmptyExposing)
      }
      let docs = []
      for i, x in items {
        docs.push(self.expose_doc(path.child("explicit", i), x.value))
      }
      let list = @pretty.text("exposing") +
        @pretty.nest(
          4,
          @pretty.line() +
          @pretty.text("(") +
          @pretty.if_break(@pretty.text(" "), @pretty.empty()) +
          @pretty.join(docs, @pretty.softline() + @pretty.text(", ")) +
          @pretty.softline() +
          @pretty.text(")"),
        )
      if import_ {
        @pretty.group(@pretty.nest(4, @pretty.line() + list))
      } else {
        @pretty.group(@pretty.text(" ") + list)
      }
    }
  }
}

///|
fn Ctx::module_doc(
  self : Ctx,
  path : @syntax.NodePath,
  m : @ast.Node[@ast.Module],
) -> @pretty.Doc raise PrintError {
  match m.value {
    NormalModule(d) | PortModule(d) => {
      let keyword = if m.value is PortModule(_) {
        "port module "
      } else {
        "module "
      }
      @pretty.text(
        keyword + module_name(path.child("moduleName", 0), d.module_name.value),
      ) +
      self.exposing_doc(path.child("exposingList", 0), d.exposing_list)
    }
    EffectModule(d) => {
      let fields = []
      if d.command is Some(c) {
        guard is_upper(c.value) else {
          raise PrintError(
            path=path.child("command", 0),
            problem=InvalidName(Upper, c.value),
          )
        }
        fields.push("command = " + c.value)
      }
      if d.subscription is Some(s) {
        guard is_upper(s.value) else {
          raise PrintError(
            path=path.child("subscription", 0),
            problem=InvalidName(Upper, s.value),
          )
        }
        fields.push("subscription = " + s.value)
      }
      guard !fields.is_empty() else {
        raise PrintError(path~, problem=NoFields)
      }
      @pretty.text(
        "effect module " +
        module_name(path.child("moduleName", 0), d.module_name.value) +
        " where { " +
        fields.join(", ") +
        " }",
      ) +
      self.exposing_doc(path.child("exposingList", 0), d.exposing_list)
    }
  }
}

///|
fn Ctx::import_doc(
  self : Ctx,
  path : @syntax.NodePath,
  i : @ast.Node[@ast.Import],
) -> @pretty.Doc raise PrintError {
  let mut d = @pretty.text(
    "import " +
    module_name(path.child("moduleName", 0), i.value.module_name.value),
  )
  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 name = module_name(p, a.value)
    guard a.value.length() == 1 else {
      raise PrintError(path=p, problem=InvalidName(Upper, name))
    }
    d = d + @pretty.text(" as " + name)
  }
  if i.value.exposing_list is Some(x) {
    d = d + self.exposing_doc(path.child("exposingList", 0), x, import_=true)
  }
  d
}

///|
/// The whole file in the elm-format layout. The `{-|` comments of
/// `File.comments` are printed: one that ends on the row before a port
/// declaration goes above that port, and the first other one is the
/// module documentation. Other comments are not printed.
fn Ctx::file_doc(self : Ctx, file : @ast.File) -> @pretty.Doc raise PrintError {
  let root = @syntax.NodePath::root()
  let port_docs : Array[@pretty.Doc?] = Array::make(
    file.declarations.length(),
    None,
  )
  let mut module_docs : @pretty.Doc? = None
  for i, c in file.comments {
    guard c.value.has_prefix("{-|") else { continue }
    let doc = documentation_doc(root.child("comments", i), c.value)
    let mut port = -1
    for j, decl in file.declarations {
      if decl.value is PortDeclaration(_) &&
        decl.range.start.row == c.range.end.row + 1 {
        port = j
        break
      }
    }
    if port >= 0 {
      port_docs[port] = Some(doc)
    } else if module_docs is None {
      module_docs = Some(doc)
    }
  }
  let mut d = self.module_doc(
    root.child("moduleDefinition", 0),
    file.module_definition,
  )
  if module_docs is Some(m) {
    d = d + @pretty.hardline() + @pretty.hardline() + m
  }
  if !file.imports.is_empty() {
    d = d + @pretty.hardline() + @pretty.hardline()
    for i, imp in file.imports {
      if i > 0 {
        d = d + @pretty.hardline()
      }
      d = d + self.import_doc(root.child("imports", i), imp)
    }
  }
  for i, decl in file.declarations {
    let infix_pair = i > 0 &&
      decl.value is InfixDeclaration(_) &&
      file.declarations[i - 1].value is InfixDeclaration(_)
    d = d +
      (if infix_pair {
        @pretty.hardline()
      } else {
        @pretty.hardline() + @pretty.hardline() + @pretty.hardline()
      })
    if port_docs[i] is Some(pd) {
      d = d + pd + @pretty.hardline()
    }
    d = d + self.declaration_doc(root.child("declarations", i), 0, decl)
  }
  d + @pretty.hardline()
}