// Typed AST for a Hunspell `.aff` file.
//
// This is an independent, clean-room model of the file format as documented in
// the Hunspell manual. No Hunspell (LGPL-2.1) source code was consulted or
// copied; this project is Apache-2.0.
//
// The AST is intentionally lossless: everything that the format distinguishes
// is kept as a `String`/`Bool`/`Int`, and pattern languages (`condition`,
// `COMPOUNDRULE`, `CHECKCOMPOUNDPATTERN`) are stored verbatim because matching
// is out of scope for this slice.

///|
/// How flags are encoded in the `.aff` and `.dic` files.
///
/// Hunspell's default (no `FLAG` line) is one character per flag.
pub(all) enum FlagType {
  /// One character per flag; used when the file has no `FLAG` line.
  SingleChar
  /// `FLAG long`: two characters per flag.
  Long
  /// `FLAG num`: a decimal number per flag.
  Num
  /// `FLAG UTF-8`: one UTF-8 character per flag.
  Utf8
} derive(Eq, @debug.Debug)

///|
/// Whether an affix is attached before (`Prefix`) or after (`Suffix`) the stem.
pub(all) enum AffixKind {
  /// The affix is attached before the stem (`PFX`).
  Prefix
  /// The affix is attached after the stem (`SFX`).
  Suffix
} derive(Eq, @debug.Debug)

///|
/// The header line of a `PFX`/`SFX` block: `PFX flag cross_product count`.
pub(all) struct AffixHeader {
  /// Whether this header declared a `PFX` or an `SFX` block.
  kind : AffixKind
  /// The flag selecting this block. Numeric `AF` alias indices are resolved to
  /// the alias string while parsing.
  flag : String
  /// `Y` means rules of this block combine with rules of the opposite kind.
  cross_product : Bool
  /// Number of rule lines declared for this block.
  count : Int
} derive(Eq, @debug.Debug)

///|
/// One `PFX`/`SFX` rule line: `PFX flag strip add condition [morph...]`.
pub(all) struct AffixRule {
  /// Whether the rule adds before or after the stem; copied from the header.
  kind : AffixKind
  /// The flag selecting this rule. Numeric `AF` alias indices are resolved to
  /// the alias string while parsing.
  flag : String
  /// Copied from the block header; only headers carry cross-product.
  cross_product : Bool
  /// Text removed from the stem; `"0"` means "strip nothing".
  strip : String
  /// Text added to the stem; `"0"` means "add nothing".
  add : String
  /// The affix condition, stored verbatim. It is a mini-pattern applied to the
  /// start of the stem for prefixes and to the end for suffixes; matching is
  /// not implemented in this slice.
  condition : String
  /// Morphological fields (`po:`, `ds:`, `is:`, ...) in source order.
  morph : Array[String]
} derive(Eq, @debug.Debug)

///|
/// One `REP` replacement pair.
///
/// `from`/`to` are stored verbatim, so the Hunspell convention of `_` for a
/// space is preserved and must be interpreted by the consumer.
pub(all) struct Replacement {
  /// The text to look for.
  from : String
  /// The text to replace it with.
  to : String
} derive(Eq, @debug.Debug)

///|
/// One `PHONE` phonetic-transcription rule.
///
/// Both fields are stored verbatim. The pattern uses the Aspell/Hunspell
/// mini-language documented in the `hunspell(5)` manual (`(class)` character
/// classes, leading/trailing `-`, `<`, digit priorities, `^`, `$`); `_` as a
/// replacement means "output nothing".
pub(all) struct PhoneRule {
  /// The Aspell/Hunspell phonetic pattern, verbatim.
  pattern : String
  /// The replacement text; `_` means "output nothing".
  replacement : String
} derive(Eq, @debug.Debug)

///|
/// One `ICONV`/`OCONV` conversion pair.
///
/// `from`/`to` are stored verbatim. For `ICONV` a trailing `_` in `from` is the
/// Hunspell "end of word" marker (`ICONV _ x` applies only when `` is
/// the last character of the word, as the manual's `ICONV <200D>_ `
/// example describes); the consumer strips and interprets it.
pub(all) struct Conversion {
  /// The input sequence, verbatim (a trailing `_` for `ICONV` means end of word).
  from : String
  /// The output sequence.
  to : String
} derive(Eq, @debug.Debug)

///|
/// The flag-valued directives that mark a word (or affix) as special.
///
/// `None` means the directive was absent. Values are stored verbatim (see
/// `parse_aff` for how `AF` aliases are handled).
pub(all) struct SpecialFlags {
  /// `NOSUGGEST`: never offer the word as a suggestion.
  nosuggest : String?
  /// `KEEPCASE`: the word is only correct in the exact spelling given.
  keepcase : String?
  /// `FORBIDDENWORD`: the word is always wrong, even as an affixed form.
  forbiddenword : String?
  /// `NEEDAFFIX` (or its deprecated spelling `PSEUDOROOT`): the bare stem is not
  /// a word, only an affixed form of it is.
  needaffix : String?
  /// `CIRCUMFIX`: an affix carrying this flag must be paired with one of the
  /// opposite kind carrying it too.
  circumfix : String?
  /// `ONLYINCOMPOUND`: the word is only correct inside a compound.
  onlyincompound : String?
} derive(Eq, @debug.Debug)

///|
/// A directive this slice does not model, kept so that no input is rejected
/// merely for being unknown.
pub(all) struct UnknownDirective {
  /// The directive keyword, e.g. `ICONV`.
  name : String
  /// The remaining whitespace-separated arguments, verbatim.
  args : Array[String]
  /// 1-based line number the directive appeared on.
  line : Int
} derive(Eq, @debug.Debug)

///|
/// A parsed Hunspell `.aff` file.
///
/// `compound_min` is `0` when the file has no `COMPOUNDMIN` line (Hunspell's
/// own default of `3` is a matching-time concern for a later slice). `encoding`
/// is `""` when the file has no `SET` line.
pub(all) struct AffFile {
  /// `SET`: the declared encoding; `""` when the directive is absent.
  encoding : String
  /// `FLAG`: how flags are encoded in both files.
  flag_type : FlagType
  /// `LANG`: the language code, used for the Turkic `I`/`İ` casing rules
  /// (`LANG tr`, `az`, `crh`). `""` when the directive is absent.
  lang : String
  /// Headers of every `PFX`/`SFX` block, in source order.
  affix_headers : Array[AffixHeader]
  /// Every `PFX`/`SFX` rule line, in source order.
  affix_rules : Array[AffixRule]
  /// `AF` alias table; index `0` is alias `1`.
  flag_aliases : Array[String]
  /// `AM` alias table; index `0` is alias `1`.
  morph_aliases : Array[String]
  /// `REP` replacement pairs, in source order.
  replacements : Array[Replacement]
  /// `MAP` groups, in source order. Each entry is one `MAP` body line stored
  /// verbatim: a run of single characters (and/or parenthesized character
  /// sequences) that are considered interchangeable when suggesting.
  map_groups : Array[String]
  /// `PHONE` phonetic-transcription rules, in source order.
  phone_rules : Array[PhoneRule]
  /// `ICONV` input-conversion pairs, in source order. Applied to the word being
  /// checked before any lookup (never to dictionary entries).
  iconv : Array[Conversion]
  /// `OCONV` output-conversion pairs, in source order. Applied to suggestion
  /// output only.
  oconv : Array[Conversion]
  /// `TRY`: the characters the suggestion engine may substitute or insert.
  try_chars : String
  /// `KEY`: the keyboard layout, one row per `|`-separated group.
  keyboard : String
  /// `MAXNGRAMSUGS`: how many n-gram suggestions the n-gram pass may return;
  /// `None` when the directive is absent. The manual documents that
  /// `0` switches the pass off but does not state the default, so an absent
  /// directive is kept distinct from a declared `0`.
  ///
  /// Parsed and recorded here only: no n-gram candidate generator consumes it
  /// yet, because the manual does not specify the similarity scoring (see the
  /// README's "Not implemented yet").
  max_ngram_sugs : Int?
  /// `MAXDIFF`: the n-gram similarity factor. `None` when the directive is
  /// absent; the manual documents `5` as the default value and the range
  /// `0`-`10` (`0` = fewer n-gram suggestions but at least one, `10` =
  /// `MAXNGRAMSUGS` suggestions). The value is stored verbatim, so a file that
  /// declares a value outside that range still parses.
  ///
  /// Parsed and recorded here only, like `max_ngram_sugs`.
  max_diff : Int?
  /// `ONLYMAXDIFF`: remove every "bad" n-gram suggestion instead of the default
  /// behaviour of keeping one.
  ///
  /// Parsed and recorded here only, like `max_ngram_sugs`.
  only_max_diff : Bool
  /// `IGNORE`: characters to drop before comparing words.
  ignore_chars : String
  /// `WORDCHARS`: extra characters that may appear inside a word.
  word_chars : String
  /// `CHECKSHARPS`: German `ß` may be written `SS` in an uppercased word, and a
  /// `KEEPCASE` word containing `ß` may be capitalised or uppercased (but its
  /// uppercased form may only use `SS`, never a sharp s).
  check_sharps : Bool
  /// `COMPLEXPREFIXES`: twofold prefix matching is allowed, and only one suffix
  /// level instead of the default two (the manual's wording). Hunspell 1.7.3
  /// behaves exactly this way — a probe dictionary confirmed that twofold
  /// suffix stacking stops working and twofold prefix stacking starts.
  complex_prefixes : Bool
  /// Whether a `BREAK` directive appeared at all. `BREAK 0` declares "no break
  /// points", which is different from the directive being absent (Hunspell then
  /// breaks at `-`, `^-` and `-$`).
  breaks_declared : Bool
  /// The `BREAK` patterns, verbatim. A leading `^` or trailing `$` means "strip
  /// that text at the start" or "at the end" instead of splitting there.
  break_patterns : Array[String]
  /// `COMPOUNDMIN`: minimum length of a compound part; `0` when absent.
  compound_min : Int
  /// `COMPOUNDFLAG`: the flag a word needs to be any part of a compound.
  compound_flag : String?
  /// `COMPOUNDBEGIN`: the flag a word needs to start a compound.
  compound_begin : String?
  /// `COMPOUNDEND`: the flag a word needs to end a compound.
  compound_end : String?
  /// `COMPOUNDMIDDLE`: the flag a word needs to sit inside a compound.
  compound_middle : String?
  /// `COMPOUNDRULE` patterns, verbatim (e.g. `n*mp`).
  compound_rules : Array[String]
  /// `COMPOUNDPERMITFLAG`: an affix carrying this flag may sit *inside* a
  /// compound (a prefix that is not at the very front, or a suffix that is not
  /// at the very end). Affixes at those natural edges need no permit.
  compound_permit : String?
  /// `COMPOUNDFORBIDFLAG`: an affix carrying this flag forbids compounding of
  /// the word it builds; a plain entry carrying it is removed from the first and
  /// middle positions of a compound.
  compound_forbid : String?
  /// `FORCEUCASE`: a compound whose last part carries this flag is only correct
  /// in a capitalised (or all-caps) spelling.
  forceucase : String?
  /// `COMPOUNDWORDMAX`: maximum number of parts in a compound (`0` = unlimited).
  compound_word_max : Int
  /// `COMPOUNDSYLLABLE max_syllable vowels`: Hungarian syllable-count limit;
  /// `compound_syllable_max` is `0` when the directive is absent.
  compound_syllable_max : Int
  /// The `vowels` argument of `COMPOUNDSYLLABLE`.
  compound_syllable_vowels : String
  /// `CHECKCOMPOUNDDUP`: no compound part may be repeated.
  check_compound_dup : Bool
  /// `CHECKCOMPOUNDTRIPLE`: no three identical characters across a compound
  /// boundary.
  check_compound_triple : Bool
  /// `CHECKCOMPOUNDREP`: reject a compound that a `REP` rule can turn into a
  /// dictionary word.
  check_compound_repeat : Bool
  /// `SIMPLIFIEDTRIPLE`: allow a simplified letter at a compound boundary.
  check_compound_simplified_triple : Bool
  /// `CHECKCOMPOUNDCASE`: forbid upper-case characters at a compound boundary.
  check_compound_case : Bool
  /// `CHECKCOMPOUNDPATTERN` lines, verbatim; parsed at match time.
  check_compound_pattern : Array[String]
  /// The special-flag directives, collected.
  special_flags : SpecialFlags
  /// Directives with no typed field above, in source order.
  unrecognized : Array[UnknownDirective]
} derive(Eq, @debug.Debug)