///|
/// One typed EBNF expression. Concatenations and alternations are flat.
/// Grouping parentheses are syntax only; the tree records the structure.
pub(all) enum Expr {
  Terminal(String, @model.Quote, Notes, @diagrams.Span?)
  /// A nonterminal reference by name.
  Name(String, Notes, @diagrams.Span?)
  Special(String, Notes, @diagrams.Span?)
  Concat(ReadOnlyArray[Expr])
  Alt(ReadOnlyArray[Expr])
  Optional(Expr, @diagrams.Span?)
  ZeroOrMore(Expr, @diagrams.Span?)
  OneOrMore(Expr, @diagrams.Span?)
  /// `n * x`, with the decimal count spelling.
  Count(String, Expr, @diagrams.Span?)
  /// `- x`: an exclusion of `x` from the previous item of its concatenation.
  Not(Expr, @diagrams.Span?)
} derive(Eq, Debug)

///|
/// The note above and the note below a leaf term.
pub struct Notes {
  above : String?
  below : String?
} derive(Eq, Debug)

///|
pub let no_note : Notes = { above: None, below: None, }

///|
pub extend Expr with Eq::{not_equal, equal}

///|
pub extend Expr with @debug.Debug::{to_repr}

///|
pub extend Notes with Eq::{not_equal, equal}

///|
pub extend Notes with @debug.Debug::{to_repr}

///|
pub fn Expr::without_spans(self : Expr) -> Expr {
  fn all(items : ReadOnlyArray[Expr]) -> ReadOnlyArray[Expr] {
    ReadOnlyArray::from_iter(items.iter().map(e => e.without_spans()))
  }
  match self {
    Terminal(t, q, n, _) => Terminal(t, q, n, None)
    Name(t, n, _) => Name(t, n, None)
    Special(t, n, _) => Special(t, n, None)
    Concat(items) => Concat(all(items))
    Alt(items) => Alt(all(items))
    Optional(e, _) => Optional(e.without_spans(), None)
    ZeroOrMore(e, _) => ZeroOrMore(e.without_spans(), None)
    OneOrMore(e, _) => OneOrMore(e.without_spans(), None)
    Count(c, e, _) => Count(c, e.without_spans(), None)
    Not(e, _) => Not(e.without_spans(), None)
  }
}

///|
pub struct Rule {
  name : String
  body : Expr?
  above : String?
  below : String?
  span : @diagrams.Span?
} derive(Eq, Debug)

///|
/// A rule from typed values, without a source location.
pub fn Rule::new(
  name : String,
  body? : Expr,
  above? : String,
  below? : String,
) -> Rule {
  { name, body, above, below, span: None, }
}

///|
pub extend Rule with Eq::{not_equal, equal}

///|
pub extend Rule with @debug.Debug::{to_repr}

///|
pub(all) enum Item {
  Rule(Rule)
  /// A free-standing note, one entry per line.
  Note(ReadOnlyArray[String], @diagrams.Span?)
} derive(Eq, Debug)

///|
pub extend Item with Eq::{not_equal, equal}

///|
pub extend Item with @debug.Debug::{to_repr}

///|
pub struct Document {
  name : String?
  statements : ReadOnlyArray[@envelope.Statement]
  items : ReadOnlyArray[Item]
} derive(Eq, Debug)

///|
pub extend Document with Eq::{not_equal, equal}

///|
pub extend Document with @debug.Debug::{to_repr}

///|
pub fn Document::new(
  items : ArrayView[Item],
  name? : String,
  statements~ : ArrayView[@envelope.Statement] = [],
) -> Document {
  {
    name,
    statements: ReadOnlyArray::from_array(statements),
    items: ReadOnlyArray::from_array(items),
  }
}

///|
pub fn Document::without_spans(self : Document) -> Document {
  {
    ..self,
    items: ReadOnlyArray::from_iter(
      self.items
      .iter()
      .map(item => {
        match item {
          Rule(rule) =>
            Rule({
              ..rule,
              body: rule.body.map(e => e.without_spans()),
              span: None,
            })
          Note(lines, _) => Note(lines, None)
        }
      }),
    ),
  }
}

///|
pub fn Document::logical_equal(self : Document, other : Document) -> Bool {
  self.without_spans() == other.without_spans()
}