///|
using @trie {type Trie}

///|
/// Matched option handler that used to interpret options.
/// 
///  Unit - handle with callback  
/// 
///  String - handle associated value with callback
/// 
///  Set_string - set option to associated value 
/// 
///  Set - set reference to true
/// 
///  Clear - set reference to false
pub(all) enum Spec {
  Unit(() -> Unit raise)
  String((String) -> Unit raise)
  Set_string(Ref[String])
  Set(Ref[Bool])
  Clear(Ref[Bool])
}

///|
fn interpret(
  trie : @trie.T[Spec],
  xs : Array[String],
  fallback : (String) -> Unit raise,
) -> Unit raise {
  loop xs[:] {
    [] => ()
    [x, .. xs] =>
      match trie.lookup(x) {
        None => {
          fallback(x)
          continue xs
        }
        Some(spec) =>
          match (spec, xs) {
            (String(f), [y, .. ys]) => {
              f(y)
              continue ys
            }
            (Set_string(r), [y, .. ys]) => {
              r.val = y
              continue ys
            }
            (String(_), []) | (Set_string(_), []) =>
              raise ErrorMsg("missing argument for \{x}")
            (Set(r), _) => {
              r.val = true
              continue xs
            }
            (Clear(r), _) => {
              r.val = false
              continue xs
            }
            (Unit(f), _) => {
              f()
              continue xs
            }
          }
      }
  }
}

///|
/// Parse argument list for CLI tools
/// 
/// Match `OptionName` or `LongOptionName` then interpret with `Spec` and 
/// use `Description` to generate `-h` or `--help` options message.
/// 
/// # Parameters
/// 
/// - speclist: (LongOptionName, OptionName, Spec, Description) of Array 
/// - rest: rest arguments handler
/// - usage_msg: synopsis usage  
/// - argv: input argument list 
/// 
/// # Exception
/// 
/// If the input arguments contains help option or invalid input, the `parse` 
/// function will raise `ErrorMsg` error.
pub fn parse(
  speclist : Array[(String, String, Spec, String)],
  rest : (String) -> Unit raise,
  usage_msg : String,
  argv : Array[String],
) -> Unit raise {
  let aux = fn(acc : (Trie[Spec], String), it) {
    let (trie, help_msg) = acc
    let (a, b, spec, help) = it
    let trie = trie.add(a, spec).add(b, spec)
    let help_msg = help_msg + "  \{a}\t\{b}\t" + help + "\n"
    (trie, help_msg)
  }
  let (trie, help_msg) = speclist.fold(
    aux,
    init=(Trie::empty(), usage_msg + "\n options:\n"),
  )
  let help_spec = Spec::Unit(() => raise ErrorMsg(help_msg))
  let trie = trie.add("--help", help_spec).add("-h", help_spec)
  interpret(trie, argv, rest)
}

///|
pub suberror ErrorMsg {
  ErrorMsg(String)
}