///|
/// The layout table for one kind of compound.
///
/// Fifteen fields, because the reference decides all of this in one `case` and
/// the decisions are not independent: whether a form may be written on one
/// line depends on whether it is at the tail of its group, which depends on
/// whether the enclosing form armours itself, and so on. Splitting them apart
/// would mean recomputing the same conditions in five places.
/// A delimiter that has two spellings.
///
/// Only `'…'` needs both: when its contents expose a `'`, it has to be written
/// `'«…»'`. The reference says this by making the field either a string or a
/// pair and picking with `car`/`cadr`; naming the two readings says the same
/// thing and puts the choice in one place.
priv struct Delim {
  guillemets : String
  plain : String
}

///|
/// The spelling to use, given whether the contents expose a quote.
fn Delim::choose(self : Delim, quoted : Bool) -> String {
  if quoted {
    self.guillemets
  } else {
    self.plain
  }
}

///|
/// The same, for a delimiter that is always spelled one way.
fn same(text : String) -> Delim {
  { guillemets: text, plain: text, }
}

///|
priv struct Shape {
  /// Whether the multi-line reading is indented from the current column rather
  /// than from the enclosing indentation.
  align : Bool
  /// Opener for the multi-line reading; a pair when a `'…'` may need `«»`.
  open : Delim
  /// Opener for the single-line reading.
  line_open : Delim
  /// Separator with no space, used at a line break.
  sep : String
  /// Separator between terms on one line.
  sep_space : String
  /// Separator before a block, which joins rather than separates.
  block_sep_space : String
  line_close : Delim
  /// `None` where the form has no closer at all.
  close : Delim?
  /// Whether the multi-line reading is still one line, with `;` separators.
  one_line_ish : Bool
  is_quotes : Bool
  /// What to pass to children as "not at the tail".
  use_non_tail : Bool
  /// Whether a child at the end of this form is at the tail of its group.
  can_tail : Bool
  /// Whether this form's own delimiters make its contents unambiguous, so a
  /// child does not need `«»`.
  wraps : Bool
  /// Whether the single-line reading is refused outright.
  no_line : Bool
  /// Whether the multi-line reading is used even when a single line would fit.
  multi_just_multi : Bool
}

///|
/// Build both readings of a term.
fn build(
  node : @ast.Node,
  target : Target,
  non_tail~ : Bool,
  head~ : Bool,
  before_block~ : Bool,
) -> Built {
  match node.it {
    Op(name) =>
      if before_block && all_colons(name) {
        twice(name + " ")
      } else {
        twice(name)
      }
    Id(_) | Kw(_) | Lit(_) | Parsed(_) => twice(atom_text(node.it))
    Alts(blocks) => build_alts(blocks, target, head~)
    Multi(groups) => build_multi(groups, target)
    _ => build_compound(node, target, non_tail~, head~)
  }
}

///|
fn build_compound(
  node : @ast.Node,
  target : Target,
  non_tail~ : Bool,
  head~ : Bool,
) -> Built {
  let children = node.children()
  let armor = target is ArmorDoc
  let prefer_multi = target is MultiDoc
  let mt = children.length() == 0
  let a_mt = armor || mt
  let a_nt = armor || (non_tail && !prefer_multi)
  let shape = match node.it {
    Group(_) =>
      (
        {
          align: true,
          open: same(""),
          line_open: same(""),
          sep: "",
          sep_space: " ",
          block_sep_space: "",
          line_close: same(""),
          close: Some(same("")),
          one_line_ish: true,
          is_quotes: false,
          use_non_tail: true,
          can_tail: !non_tail,
          wraps: false,
          no_line: false,
          multi_just_multi: false,
        } : Shape)
    Block(_) =>
      {
        align: false,
        open: same(if a_mt { ":«" } else { ":" }),
        line_open: same(if mt { ":«" } else if a_nt { ":« " } else { ": " }),
        sep: if armor {
          ";"
        } else {
          ""
        },
        sep_space: "; ",
        block_sep_space: "; ",
        line_close: same(if mt { "»" } else if a_nt { " »" } else { "" }),
        close: if a_mt {
          Some(same("»"))
        } else {
          None
        },
        one_line_ish: false,
        is_quotes: false,
        use_non_tail: true,
        can_tail: true,
        wraps: false,
        no_line: prefer_multi && (a_mt || children.length() >= 2),
        // A block is always written multi-line in multi-line mode. It must be
        // inside a group, and that group's own `or` is what lets the whole
        // thing come back onto one line when it fits.
        multi_just_multi: true,
      }
    Parens(_) => bracket_shape("(", ")")
    Brackets(_) => bracket_shape("[", "]")
    Braces(_) => bracket_shape("{", "}")
    Quotes(_) =>
      {
        align: false,
        open: { guillemets: "'«", plain: "'", },
        line_open: { guillemets: "'«", plain: "'", },
        sep: if armor {
          ";"
        } else {
          ""
        },
        sep_space: "; ",
        block_sep_space: "; ",
        line_close: { guillemets: "»'", plain: "'", },
        close: Some({ guillemets: "»'", plain: "'", }),
        one_line_ish: false,
        is_quotes: true,
        use_non_tail: true,
        can_tail: true,
        wraps: true,
        no_line: prefer_multi && children.length() >= 2,
        multi_just_multi: false,
      }
    _ => bracket_shape("(", ")")
  }
  assemble(children, shape, target, head~)
}

///|
fn bracket_shape(open : String, close : String) -> Shape {
  {
    align: false,
    open: same(open),
    line_open: same(open),
    sep: ",",
    sep_space: ", ",
    block_sep_space: ", ",
    line_close: same(close),
    close: Some(same(close)),
    one_line_ish: false,
    is_quotes: false,
    use_non_tail: false,
    can_tail: true,
    wraps: true,
    no_line: false,
    multi_just_multi: false,
  }
}

///|
/// The five readings of a compound's contents, assembled into the two the
/// caller sees.
fn assemble(
  children : Array[@ast.Node],
  shape : Shape,
  target : Target,
  head~ : Bool,
) -> Built {
  let prefer_multi = target is MultiDoc
  // One line, with separators.
  let mut singles : Array[@doc.Doc]? = Some([])
  // Multi-line, but still one line: separators are `;` rather than breaks.
  let semis : Array[@doc.Doc] = []
  // Multi-line, indented one past the opener -- the reading a bracketed form
  // falls back to before it gives up on the line entirely.
  let line_multi : Array[@doc.Doc] = []
  // Multi-line, indented two from the enclosing form.
  let multi : Array[@doc.Doc] = []
  let mut inner_quotes = false
  for i in 0.. {
        acc.push(s)
        if !last {
          acc.push(
            Str(
              if next_is_block {
                shape.block_sep_space
              } else {
                shape.sep_space
              },
            ),
          )
        }
      }
      _ => singles = None
    }
    semis.push(r.multi)
    if !last {
      semis.push(
        Str(
          if next_is_block || next_is_alts {
            shape.block_sep_space
          } else {
            shape.sep_space
          },
        ),
      )
    }
    if last {
      line_multi.push(
        if i == 0 {
          r.multi
        } else {
          Nest(1, Seq([Nl, r.multi]))
        },
      )
      multi.push(Nest(2, Seq([Nl, r.multi])))
    } else {
      line_multi.push(
        Nest(
          1,
          Seq([if i == 0 { @doc.empty } else { Nl }, r.multi, Str(shape.sep)]),
        ),
      )
      multi.push(Nest(2, Seq([Nl, r.multi, Str(shape.sep)])))
    }
  }
  let quotes = shape.is_quotes || (!shape.wraps && inner_quotes)
  // A `'…'` whose contents expose a `'` needs its guillemets; otherwise the
  // plain form reads the same and is shorter.
  let line_open = shape.line_open.choose(inner_quotes)
  let line_close = shape.line_close.choose(inner_quotes)
  let open = shape.open.choose(inner_quotes)
  let close = shape.close.map(d => d.choose(inner_quotes))
  let line : @doc.Doc? = match singles {
    Some(parts) => {
      let out : Array[@doc.Doc] = [Str(line_open)]
      for p in parts {
        out.push(p)
      }
      out.push(Str(line_close))
      Some(Seq(out))
    }
    None => None
  }
  if children.length() == 0 {
    return {
      single: line,
      multi: match line {
        Some(l) => l
        None => Seq([Str(line_open), Str(line_close)])
      },
      quotes,
      is_atom: shape.wraps,
    }
  }
  let mut body : @doc.Doc = if shape.one_line_ish {
    let out : Array[@doc.Doc] = [Str(line_open)]
    for p in semis {
      out.push(p)
    }
    out.push(Str(line_close))
    Seq(out)
  } else {
    let out : Array[@doc.Doc] = [Str(open)]
    for p in multi {
      out.push(p)
    }
    match close {
      Some(c) => {
        out.push(Nl)
        out.push(Str(c))
      }
      None => ()
    }
    Seq(out)
  }
  if shape.align {
    body = Align(body)
  }
  if prefer_multi && shape.wraps {
    let out : Array[@doc.Doc] = [Str(line_open)]
    for p in line_multi {
      out.push(p)
    }
    out.push(Str(line_close))
    body = Or(body, Align(Seq(out)))
  }
  match line {
    Some(l) =>
      if !shape.no_line &&
        !shape.multi_just_multi &&
        (shape.wraps || shape.one_line_ish || multi.length() == 1) {
        body = Or(l, body)
      }
    None => ()
  }
  {
    single: if shape.no_line {
      None
    } else {
      line
    },
    multi: body,
    quotes,
    is_atom: shape.wraps,
  }
}