///|
/// A filter: the value left of `|`, the arguments in parentheses.
pub type Filter = (Call, Value, Array[Value], Map[String, Value]) -> Value raise Refused

///|
/// A test: the value left of `is`, the arguments after the name.
pub type Test = (Call, Value, Array[Value]) -> Bool raise Refused

///|
/// What happens when an undefined value is used, Jinja's three answers:
/// `Lenient` prints nothing and iterates as empty but refuses attribute
/// access; `Chainable` also lets attributes of it be undefined in turn;
/// `Strict` refuses every use except asking whether it is defined.
pub(all) enum Undefined {
  Lenient
  Chainable
  Strict
} derive(Eq)

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

///|
/// What an extension can reach of the render in flight: whether output is
/// being escaped, the undefined policy, the other filters and tests, the
/// translation seam, and the bound on `range`.
pub struct Call {
  /// Whether `{{ }}` escapes here; a filter that builds markup reads it.
  autoescape : Bool
  /// How an undefined value behaves when used.
  undefined : Undefined
  /// The largest sequence `range` may build.
  range : Int32
  priv filters : Map[String, Filter]
  priv tests : Map[String, Test]
  priv translate : ((String, String?, Int32?) -> String?)?
  priv draws : Ref[Int32]
}

///|
/// The engine builds one per render; a test of an extension builds its own.
pub fn Call::new(
  autoescape~ : Bool = true,
  undefined~ : Undefined = Strict,
  range~ : Int32 = 100000,
  filters~ : Map[String, Filter] = Map([]),
  tests~ : Map[String, Test] = Map([]),
  translate? : (String, String?, Int32?) -> String?,
) -> Call {
  { autoescape, undefined, range, filters, tests, translate, draws: Ref(0), }
}

///|
/// The same render with escaping turned on or off.
pub fn Call::escaping(self : Call, autoescape : Bool) -> Call {
  { ..self, autoescape, }
}

///|
/// Apply the filter registered under `name`.
pub fn Call::filter(
  self : Call,
  name : String,
  value : Value,
  args : Array[Value],
  named : Map[String, Value],
) -> Value raise Refused {
  match self.filters.get(name) {
    Some(run) => run(self, value, args, named)
    None => raise unknown("filter", name)
  }
}

///|
/// Apply the test registered under `name`; `test` itself is a keyword.
pub fn Call::check(
  self : Call,
  name : String,
  value : Value,
  args : Array[Value],
) -> Bool raise Refused {
  match self.tests.get(name) {
    Some(run) => run(self, value, args)
    None => raise unknown("test", name)
  }
}

///|
/// Whether a filter is registered under `name`.
pub fn Call::has_filter(self : Call, name : String) -> Bool {
  self.filters.contains(name)
}

///|
/// Whether a test is registered under `name`.
pub fn Call::has_test(self : Call, name : String) -> Bool {
  self.tests.contains(name)
}

///|
/// Ask the translation seam for `msgid` in `context`, plural form for
/// `count`. `None` when there is no seam or it has no translation.
pub fn Call::translate(
  self : Call,
  msgid : String,
  context? : String,
  count? : Int32,
) -> String? {
  match self.translate {
    Some(seam) => seam(msgid, context, count)
    None => None
  }
}

///|
/// A number that differs from call to call within a render and repeats on
/// the next render, for filters such as `random` that must give the same
/// page twice.
pub fn Call::draw(self : Call) -> Int32 {
  self.draws.val += 1
  self.draws.val
}

///|
/// A value as text, Python's `str`: an undefined value is empty, or
/// refused under the strict policy.
pub fn Call::text(self : Call, value : Value) -> String raise Refused {
  match value {
    Undefined(name) if self.refuses(name) => raise unknown("name", name)
    _ => value.to_string()
  }
}

///|
/// Whether using an undefined value of this name is refused. The strict
/// policy refuses a name that was looked up and missing; the implicit else
/// of an inline `if` has no name and is never refused.
pub fn Call::refuses(self : Call, name : String) -> Bool {
  self.undefined == Strict && name != ""
}

///|
/// The items a loop over the value walks; an undefined value is empty, or
/// refused under the strict policy.
pub fn Call::items(self : Call, value : Value) -> Array[Value] raise Refused {
  match value {
    Undefined(name) =>
      if self.refuses(name) {
        raise unknown("name", name)
      } else {
        []
      }
    _ => value.iter()
  }
}

///|
/// Whether the value counts as true; an undefined value is false, or
/// refused under the strict policy.
pub fn Call::truthy(self : Call, value : Value) -> Bool raise Refused {
  match value {
    Undefined(name) if self.refuses(name) => raise unknown("name", name)
    _ => value.truthy()
  }
}