///|
/// The kind of a YAML value that a lexicon key needs.
pub(all) enum ValueKind {
  Text
  List
  Mapping
  Number
} derive(Eq, Debug)

///|
/// Raised when a lexicon value breaks a rule.
pub(all) suberror LexiconError {
  /// A word that is empty, or that has white space, a comma or a full stop.
  InvalidWord(String)
  /// A phrase that is empty, that has a comma, a full stop or a line break,
  /// or whose words are not separated by single spaces.
  InvalidPhrase(String)
  /// A heading prefix that is empty or that has a line break.
  InvalidHeadingPrefix(String)
  /// A heading level outside 1 to 6.
  InvalidHeadingLevel(Int32)
  /// The lexicon file is not YAML, or not a YAML mapping.
  InvalidLexiconFile(line~ : Int32, message~ : String)
  /// A key that a lexicon file does not have.
  UnknownKey(key~ : String, line~ : Int32)
  /// A value of `key` that is not of the kind that the key needs.
  WrongType(key~ : String, line~ : Int32, expected~ : ValueKind)
  /// An entry of `key` that breaks the rule of its kind.
  InvalidEntry(key~ : String, line~ : Int32, error~ : LexiconError)
} derive(Eq, Debug)

///|
fn has_space_char(text : String) -> Bool {
  for i in 0.. Word raise LexiconError {
  if text.is_empty() ||
    has_space_char(text) ||
    text.contains(",") ||
    text.contains(".") {
    raise InvalidWord(text)
  }
  { text, }
}

///|
/// The word as written.
pub fn Word::to_string(self : Word) -> String {
  self.text
}

///|
/// The word in lower case, as the parser matches it.
pub fn Word::lower(self : Word) -> String {
  self.text.to_lower()
}

///|
pub impl Eq for Word with fn equal(self, other) {
  self.lower() == other.lower()
}

///|
pub impl Hash for Word with fn hash_combine(self, hasher) {
  hasher.combine(self.lower())
}

///|
pub impl Show for Word with fn output(self, logger) {
  logger.write_string(self.text)
}

///|
pub impl @debug.Debug for Word with fn to_repr(self) {
  @debug.Repr::string(self.text)
}

///|
/// A word that the library writes in code.
fn known_word(text : String) -> Word {
  { text, }
}

///|
/// One or more words separated by single spaces, such as a system noun or a
/// lead-in phrase, with no comma, full stop or line break. A phrase keeps its
/// text as written and compares without letter case.
struct Phrase {
  text : String
}

///|
pub fn Phrase::Phrase(text : String) -> Phrase raise LexiconError {
  let words = text.split(" ").collect()
  let ok = !text.is_empty() &&
    words.iter().all(w => w.length() > 0 && !has_space_char(w.to_owned())) &&
    !text.contains(",") &&
    !text.contains(".")
  if !ok {
    raise InvalidPhrase(text)
  }
  { text, }
}

///|
/// The phrase as written.
pub fn Phrase::to_string(self : Phrase) -> String {
  self.text
}

///|
/// The phrase in lower case, as the parser matches it.
pub fn Phrase::lower(self : Phrase) -> String {
  self.text.to_lower()
}

///|
pub impl Eq for Phrase with fn equal(self, other) {
  self.lower() == other.lower()
}

///|
pub impl Hash for Phrase with fn hash_combine(self, hasher) {
  hasher.combine(self.lower())
}

///|
pub impl Show for Phrase with fn output(self, logger) {
  logger.write_string(self.text)
}

///|
pub impl @debug.Debug for Phrase with fn to_repr(self) {
  @debug.Repr::string(self.text)
}

///|
/// The start of a Markdown heading text that marks a requirement, such as
/// `Requirement:`: not empty, with no line break.
struct HeadingPrefix {
  text : String
} derive(Eq, Hash, Debug)

///|
pub fn HeadingPrefix::HeadingPrefix(
  text : String,
) -> HeadingPrefix raise LexiconError {
  if text.is_empty() || text.contains("\n") || text.contains("\r") {
    raise InvalidHeadingPrefix(text)
  }
  { text, }
}

///|
pub fn HeadingPrefix::to_string(self : HeadingPrefix) -> String {
  self.text
}

///|
/// A Markdown heading that starts a requirement block: a heading of `level`
/// whose text starts with `prefix`, with letter case. The rest of the text
/// names the block.
struct HeadingRule {
  level : @mdc.HeadingLevel
  prefix : HeadingPrefix
} derive(Eq, Hash, Debug)

///|
pub fn HeadingRule::HeadingRule(
  level : @mdc.HeadingLevel,
  prefix : HeadingPrefix,
) -> HeadingRule {
  { level, prefix, }
}

///|
pub fn HeadingRule::level(self : HeadingRule) -> @mdc.HeadingLevel {
  self.level
}

///|
pub fn HeadingRule::prefix(self : HeadingRule) -> HeadingPrefix {
  self.prefix
}

///|
/// The block name of a heading text that matches the rule.
fn HeadingRule::name_of(
  self : HeadingRule,
  level : @mdc.HeadingLevel,
  text : String,
) -> String? {
  guard level == self.level && text.has_prefix(self.prefix.text) else {
    return None
  }
  // A heading with the prefix and no name names no block.
  let name = text[self.prefix.text.length():].trim(chars=" \t").to_owned()
  if name.is_empty() {
    None
  } else {
    Some(name)
  }
}

///|
/// A dialect adds vocabulary to the EARS grammar: modal verbs, subject
/// openers (determiners and pronouns), system nouns, lead-in phrases and
/// Markdown requirement headings. Every method except `name` has a default,
/// so a dialect gives only what it adds. Several dialects combine: the parser
/// uses the union of their vocabularies.
pub(open) trait Dialect {
  /// The dialect name, used in reports.
  fn name(Self) -> String
  /// Modal verbs besides `shall`, such as `must`.
  fn modals(Self) -> Array[Word] = _
  /// Determiners that can start a subject besides `the`, such as `each`.
  fn determiners(Self) -> Array[Word] = _
  /// Pronouns that stand for a system, such as `it`.
  fn pronouns(Self) -> Array[Word] = _
  /// System nouns that start a subject without a determiner, such as a
  /// product name.
  fn system_nouns(Self) -> Array[Phrase] = _
  /// Phrases that can come before the clauses of a sentence, such as
  /// `otherwise`.
  fn lead_ins(Self) -> Array[Phrase] = _
  /// Markdown headings that start a requirement block.
  fn headings(Self) -> Array[HeadingRule] = _
}

///|
impl Dialect with fn modals(_) {
  []
}

///|
impl Dialect with fn determiners(_) {
  []
}

///|
impl Dialect with fn pronouns(_) {
  []
}

///|
impl Dialect with fn system_nouns(_) {
  []
}

///|
impl Dialect with fn lead_ins(_) {
  []
}

///|
impl Dialect with fn headings(_) {
  []
}

///|
/// A dialect defined as data: a name and lists of vocabulary. A caller
/// builds one in code or reads one from a lexicon file with
/// `Lexicon::from_yaml`.
struct Lexicon {
  name : String
  modals : ReadOnlyArray[Word]
  determiners : ReadOnlyArray[Word]
  pronouns : ReadOnlyArray[Word]
  systems : ReadOnlyArray[Phrase]
  lead_ins : ReadOnlyArray[Phrase]
  headings : ReadOnlyArray[HeadingRule]
} derive(Eq, Debug)

///|
/// A lexicon. The name is free text for reports.
pub fn Lexicon::Lexicon(
  name~ : String = "lexicon",
  modals~ : ArrayView[Word] = [],
  determiners~ : ArrayView[Word] = [],
  pronouns~ : ArrayView[Word] = [],
  systems~ : ArrayView[Phrase] = [],
  lead_ins~ : ArrayView[Phrase] = [],
  headings~ : ArrayView[HeadingRule] = [],
) -> Lexicon {
  {
    name,
    modals: ReadOnlyArray::from_array(modals.to_owned()),
    determiners: ReadOnlyArray::from_array(determiners.to_owned()),
    pronouns: ReadOnlyArray::from_array(pronouns.to_owned()),
    systems: ReadOnlyArray::from_array(systems.to_owned()),
    lead_ins: ReadOnlyArray::from_array(lead_ins.to_owned()),
    headings: ReadOnlyArray::from_array(headings.to_owned()),
  }
}

///|
pub impl Dialect for Lexicon with fn name(self) {
  self.name
}

///|
pub impl Dialect for Lexicon with fn modals(self) {
  self.modals.iter().collect()
}

///|
pub impl Dialect for Lexicon with fn determiners(self) {
  self.determiners.iter().collect()
}

///|
pub impl Dialect for Lexicon with fn pronouns(self) {
  self.pronouns.iter().collect()
}

///|
pub impl Dialect for Lexicon with fn system_nouns(self) {
  self.systems.iter().collect()
}

///|
pub impl Dialect for Lexicon with fn lead_ins(self) {
  self.lead_ins.iter().collect()
}

///|
pub impl Dialect for Lexicon with fn headings(self) {
  self.headings.iter().collect()
}

///|
/// The OpenSpec dialect: the modal verbs `shall` and `must`, the usual
/// determiners and pronouns, the lead-in `otherwise`, and the heading rule
/// `### Requirement:`. Project words belong in a project lexicon.
pub fn openspec_dialect() -> Lexicon {
  let words = (texts : Array[String]) => texts.map(known_word)
  Lexicon(
    name="openspec",
    modals=words(["shall", "must"]),
    determiners=words([
      "a", "an", "each", "every", "any", "these", "those", "this", "that", "its",
      "their",
    ]),
    pronouns=words(["it", "they"]),
    lead_ins=[{ text: "otherwise", }],
    headings=[{ level: H3, prefix: { text: "Requirement:", }, }],
  )
}

///|
/// The merged vocabulary of the active dialects, in lower case. `shall` and
/// `the` are always there. Phrases are sorted longest first, so that the
/// longest match wins.
priv struct Vocabulary {
  modals : Array[String]
  determiners : Array[String]
  pronouns : Array[String]
  systems : Array[String]
  lead_ins : Array[String]
  headings : Array[HeadingRule]
  /// A dialect adds vocabulary. Only then does `not` negate a modal verb,
  /// and does a clause take every comma that a subject word follows up to
  /// the last one before the modal verb; strict EARS keeps both as before.
  extended : Bool
}

///|
fn add_unique(out : Array[String], text : String) -> Unit {
  if !out.contains(text) {
    out.push(text)
  }
}

///|
fn longest_first(items : Array[String]) -> Array[String] {
  let out = items.copy()
  out.sort_by((a, b) => {
    let c = b.length().compare(a.length())
    if c != 0 {
      c
    } else {
      a.compare(b)
    }
  })
  out
}

///|
fn Vocabulary::of(dialects : ArrayView[&Dialect]) -> Vocabulary {
  let modals = ["shall"]
  let determiners = ["the"]
  let pronouns = []
  let systems = []
  let lead_ins = []
  let headings : Array[HeadingRule] = []
  for d in dialects {
    for w in d.modals() {
      add_unique(modals, w.lower())
    }
    for w in d.determiners() {
      add_unique(determiners, w.lower())
    }
    for w in d.pronouns() {
      add_unique(pronouns, w.lower())
    }
    for p in d.system_nouns() {
      add_unique(systems, p.lower())
    }
    for p in d.lead_ins() {
      add_unique(lead_ins, p.lower())
    }
    for h in d.headings() {
      if !headings.contains(h) {
        headings.push(h)
      }
    }
  }
  let extended = modals.length() > 1 ||
    determiners.length() > 1 ||
    !pronouns.is_empty() ||
    !systems.is_empty() ||
    !lead_ins.is_empty() ||
    !headings.is_empty()
  {
    modals,
    determiners,
    pronouns,
    systems: longest_first(systems),
    lead_ins: longest_first(lead_ins),
    headings,
    extended,
  }
}

///|
/// The vocabulary of strict EARS: `shall` and `the` only.
fn Vocabulary::strict() -> Vocabulary {
  Vocabulary::of([])
}

///|
/// The length of the first item of `items` that starts `text` as whole
/// words, or `None`.
fn match_at_start(text : String, items : Array[String]) -> Int32? {
  for item in items {
    if starts_word(text, item) {
      return Some(item.length())
    }
  }
  None
}

///|
/// The kind of subject that starts `text`, with the length of its first part.
priv enum SubjectStart {
  StartsNoun(Int32)
  StartsPronoun(Int32)
  StartsDeterminer(Int32)
}

///|
fn Vocabulary::subject_at_start(
  self : Vocabulary,
  text : String,
) -> SubjectStart? {
  match match_at_start(text, self.systems) {
    Some(n) => return Some(StartsNoun(n))
    None => ()
  }
  match match_at_start(text, self.pronouns) {
    Some(n) => return Some(StartsPronoun(n))
    None => ()
  }
  match match_at_start(text, self.determiners) {
    Some(n) => Some(StartsDeterminer(n))
    None => None
  }
}

///|
fn Vocabulary::starts_subject(self : Vocabulary, text : String) -> Bool {
  self.subject_at_start(text) is Some(_)
}

///|
/// The modal verb that starts at `at` in `text` as a whole word, with its
/// length.
fn Vocabulary::modal_at(self : Vocabulary, text : String, at : Int32) -> Int32? {
  for m in self.modals {
    if starts_word_at(text, at, m) {
      return Some(m.length())
    }
  }
  None
}

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

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

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

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

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

///|
pub extend Word with Hash::{hash, hash_combine}

///|
pub extend Word with Show::{output}

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

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

///|
pub extend Phrase with Hash::{hash, hash_combine}

///|
pub extend Phrase with Show::{output}

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

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

///|
pub extend HeadingPrefix with Hash::{hash, hash_combine}

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

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

///|
pub extend HeadingRule with Hash::{hash, hash_combine}

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

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

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

///|
pub extend Lexicon with Dialect::{
  name,
  headings,
  modals,
  pronouns,
  lead_ins,
  determiners,
  system_nouns,
}