///|
/// The module header, from `module` (or `port module`, `effect module`) to
/// the end of its exposing list.
///
/// - `module_name` is the dotted name, such as `Some("Html.Events")`.
/// - `exposing` lists the exposed names in source order. It is empty for
///   `exposing (..)`. A type exposed with `(..)` gives only its name.
/// - `tokens` are the header's tokens, with their trivia.
/// - `span` runs from the first token to the end of the last token.
pub(all) struct ModuleHeaderCst {
  module_name : String?
  exposing : Array[String]
  tokens : Array[@scanner.Token]
  span : @scanner.Span
} derive(Eq, Debug)

///|
/// One import declaration.
///
/// - `module_name` is the dotted name of the imported module.
/// - `exposing` is `None` when there is no `exposing` clause, and
///   `Some([])` for `exposing (..)`. Otherwise it lists the exposed names.
/// - `tokens` and `span` cover the declaration, alias included.
pub(all) struct ImportCst {
  module_name : String
  exposing : Array[String]?
  tokens : Array[@scanner.Token]
  span : @scanner.Span
} derive(Eq, Debug)

///|
/// A top-level function or value declaration.
///
/// - `args` has one short text per argument pattern: the variable name, `_`,
///   `()`, the constructor name of a constructor pattern, or `_` for any
///   other pattern.
/// - `tokens` and `span` cover the signature (when there is one) and the
///   definition. A doc comment is trivia of the first token; `span` does not
///   include it.
pub(all) struct FunctionCst {
  name : String
  args : Array[String]
  tokens : Array[@scanner.Token]
  span : @scanner.Span
} derive(Eq, Debug)

///|
/// A `type alias` declaration. `type_params` are the type variable names,
/// in order.
pub(all) struct TypeAliasCst {
  name : String
  type_params : Array[String]
  tokens : Array[@scanner.Token]
  span : @scanner.Span
} derive(Eq, Debug)

///|
/// A custom type declaration (`type T = A | B`). `type_params` are the type
/// variable names, in order.
pub(all) struct UnionTypeCst {
  name : String
  type_params : Array[String]
  tokens : Array[@scanner.Token]
  span : @scanner.Span
} derive(Eq, Debug)

///|
/// A top-level declaration in the CST. Ports, infix declarations and
/// destructurings have no CST form, so they do not show here.
pub(all) enum DeclarationCst {
  Function(FunctionCst)
  TypeAlias(TypeAliasCst)
  UnionType(UnionTypeCst)
} derive(Eq, Debug)

///|
/// The concrete syntax tree of a module: every token, with its trivia, plus
/// the header, imports and top-level declarations as token ranges.
///
/// - `tokens` is the whole token stream, so it keeps every comment and all
///   whitespace except in a text with no tokens.
/// - `header` is `None` when the module header is missing or malformed.
/// - A declaration that does not parse is left out of `imports` and
///   `declarations`, and reported as a diagnostic.
/// - `span` runs from the first token to the end of the last token; it is
///   `None` when there is no token.
///
/// The parser makes it (`@parser.ParseResult::cst`). It is `None` there only
/// when the scan fails.
///
/// ```mbt check
/// test {
///   let source = @scanner.SourceText::new(
///     (
///       #|module Shapes exposing (Shape(..), area)
///       #|
///       #|import Html exposing (text)
///       #|
///       #|type Shape a = Circle a | Square a
///       #|
///       #|type alias Size = Float
///       #|
///       #|area : Shape Float -> Float
///       #|area shape = 0
///     ),
///   )
///   let result = @parser.parse_module(source, @scanner.DefaultScanner::new())
///   guard result.cst is Some(module_cst) else { fail("no CST") }
///   debug_inspect(
///     module_cst.header.map(h => (h.module_name, h.exposing)),
///     content="Some((Some(\"Shapes\"), [\"Shape\", \"area\"]))",
///   )
///   debug_inspect(
///     module_cst.imports.map(i => (i.module_name, i.exposing)),
///     content="[(\"Html\", Some([\"text\"]))]",
///   )
///   let declarations = module_cst.declarations.map(d => {
///     match d {
///       Function(f) => ("function", f.name, f.args)
///       TypeAlias(a) => ("alias", a.name, a.type_params)
///       UnionType(t) => ("type", t.name, t.type_params)
///     }
///   })
///   debug_inspect(
///     declarations,
///     content=(
///       #|[
///       #|  ("type", "Shape", ["a"]),
///       #|  ("alias", "Size", []),
///       #|  ("function", "area", ["shape"]),
///       #|]
///     ),
///   )
///   // The function covers its signature and its definition.
///   guard module_cst.declarations[2] is Function(area) else { fail("no area") }
///   inspect(area.tokens[0].lexeme, content="area")
///   debug_inspect((area.span.start.line, area.span.end.line), content="(9, 10)")
/// }
/// ```
pub(all) struct ModuleCst {
  tokens : Array[@scanner.Token]
  /// The trivia of a text with no tokens (only whitespace and comments).
  /// Empty when there are tokens: then all trivia belongs to tokens.
  trivia : Array[@scanner.Trivia]
  header : ModuleHeaderCst?
  imports : Array[ImportCst]
  declarations : Array[DeclarationCst]
  span : @scanner.Span?
} derive(Eq, Debug)

///|
/// The source text of the module: each token's `trivia_before`, `lexeme`
/// and `trivia_after` in order, or `trivia` when there are no tokens. For a
/// text that scans, this is the text, byte for byte.
///
/// ```mbt check
/// test {
///   let text = "module Main exposing (..)\r\n\r\n-- note\r\nx = 1\r\n"
///   let result = @parser.parse_module(
///     @scanner.SourceText::new(text),
///     @scanner.DefaultScanner::new(),
///   )
///   debug_inspect(
///     result.cst.map(c => c.to_source() == text),
///     content="Some(true)",
///   )
/// }
/// ```
pub fn ModuleCst::to_source(self : ModuleCst) -> String {
  let out = StringBuilder()
  if self.tokens.is_empty() {
    write_trivia(out, self.trivia)
  }
  for token in self.tokens {
    write_trivia(out, token.trivia_before)
    out.write_string(token.lexeme)
    write_trivia(out, token.trivia_after)
  }
  out.to_string()
}

///|
fn write_trivia(out : StringBuilder, trivia : Array[@scanner.Trivia]) -> Unit {
  for t in trivia {
    match t {
      Whitespace(text, _) | Newline(text, _) => out.write_string(text)
      Comment(c) => out.write_string(c.text)
    }
  }
}