///|
pub(all) struct TraceEvent {
  kind : String
  label : String
  span : Span
  matched : Bool?
} derive(Eq, Debug)

///|
priv struct Runtime {
  message : Message
  budget : Budget
  trace : Array[TraceEvent]
  variables : Map[String, String]
  captures : Array[String]
  variables_enabled : Bool
  options : ExecutionOptions
}

///|
fn Runtime::record(
  self : Runtime,
  kind : String,
  label : String,
  span : Span,
  matched : Bool?,
) -> Unit raise SieveError {
  if self.trace.length() >= self.budget.limits.steps {
    fail("limit.trace", "trace event limit exceeded", span)
  }
  self.trace.push({ kind, label, span, matched })
}

///|
fn mode_from(a : Arguments) -> MatchType {
  if a.flags.contains("contains") {
    Contains
  } else if a.flags.contains("matches") {
    Matches
  } else {
    Is
  }
}

///|
fn comparator_from(a : Arguments) -> Comparator {
  if a.options.get("comparator") == Some("i;octet") {
    Octet
  } else {
    AsciiCasemap
  }
}

///|
fn address_part(mailbox : Mailbox, a : Arguments) -> String {
  if a.flags.contains("localpart") {
    mailbox.localpart
  } else if a.flags.contains("domain") {
    mailbox.domain
  } else {
    mailbox.localpart + "@" + mailbox.domain
  }
}

///|
fn Runtime::test_values(
  self : Runtime,
  name : String,
  fields : Array[String],
  a : Arguments,
  span : Span,
) -> Array[String] raise SieveError {
  let values : Array[String] = []
  for field in fields {
    self.budget.step(span)
    self.budget.compare(self.message.headers.length() + 1, span)
    let raw = if name == "envelope" {
      if ascii_lower(field) == "from" {
        [self.message.envelope_from]
      } else {
        self.message.envelope_to
      }
    } else {
      self.message.header_values(field)
    }
    if name == "header" {
      for value in raw {
        self.budget.compare(value.length() + 1, span)
        values.push(value)
      }
    } else {
      for value in raw {
        self.budget.compare(value.length() + 1, span)
        if value.is_empty() && name == "envelope" {
          if !a.flags.contains("count") {
            values.push("")
          }
        } else {
          for mailbox in extract_mailboxes(value, limits=self.budget.limits) {
            if a.flags.contains("count") {
              values.push(mailbox.address)
              continue
            }
            match selected_address_part(mailbox, a, self.options) {
              Some(value) => values.push(value)
              None => ()
            }
          }
        }
      }
    }
  }
  values
}

///|
fn Runtime::predicate(
  self : Runtime,
  name : String,
  a : Arguments,
  span : Span,
) -> Bool raise SieveError {
  match name {
    "true" => true
    "false" => false
    "exists" => {
      for field in self.expanded_strings(a.values[0], span) {
        self.budget.step(span)
        self.budget.compare(self.message.headers.length() + 1, span)
        if self.message.header_values(field).is_empty() {
          return false
        }
      }
      true
    }
    "size" => {
      let n = match a.values[0] {
        Quantity(n) => n
        _ => 0
      }
      if a.flags.contains("over") {
        self.message.size > n
      } else {
        self.message.size < n
      }
    }
    "header" | "address" | "envelope" | "string" => {
      let fields = self.expanded_strings(a.values[0], span)
      let keys = self.expanded_strings(a.values[1], span)
      let values = if name == "string" {
        if a.flags.contains("count") {
          fields.filter(value => !value.is_empty())
        } else {
          fields
        }
      } else {
        self.test_values(name, fields, a, span)
      }
      self.match_collection(values, keys, a, span)
    }
    _ => {
      fail("runtime.test", "unsupported predicate reached evaluator", span)
      false
    }
  }
}

///|
fn Runtime::condition(self : Runtime, expr : Test) -> Bool raise SieveError {
  match expr {
    Not(child, span) => {
      self.budget.step(span)
      let result = !self.condition(child)
      self.record("test", "not", span, Some(result))
      result
    }
    AnyOf(children, span) => {
      self.budget.step(span)
      let mut result = false
      for child in children {
        if self.condition(child) {
          result = true
          break
        }
      }
      self.record("test", "anyof", span, Some(result))
      result
    }
    AllOf(children, span) => {
      self.budget.step(span)
      let mut result = true
      for child in children {
        if !self.condition(child) {
          result = false
          break
        }
      }
      self.record("test", "allof", span, Some(result))
      result
    }
    Call(name, args, span) => {
      self.budget.step(span)
      let result = self.predicate(name, split_args(args, span), span)
      self.record("test", name, span, Some(result))
      result
    }
  }
}

///|
pub fn test_expression(
  source : String,
  message : Message,
  capabilities? : Array[String] = [],
  limits? : Limits = Limits::default(),
  options? : ExecutionOptions = ExecutionOptions::default(),
) -> (Bool, Array[TraceEvent]) raise SieveError {
  options.check()
  let parser : Parser = { tokens: tokenize(source, limits~), pos: 0, limits }
  let expr = parser.test_expr(0)
  if parser.current().kind != End {
    fail(
      "parse.trailing",
      "unexpected input after condition",
      parser.current().span,
    )
  }
  for cap in capabilities {
    if !supported_capabilities().contains(cap) {
      fail("check.capability", "unsupported capability", origin())
    }
  }
  check_test(expr, capabilities)
  let checked = Message::new(
    message.headers,
    message.size,
    envelope_from=message.envelope_from,
    envelope_to=message.envelope_to,
    limits~,
  )
  let rt : Runtime = {
    message: checked,
    budget: new_budget(limits),
    trace: [],
    variables: Map([]),
    captures: Array::make(10, ""),
    variables_enabled: capabilities.contains("variables"),
    options,
  }
  let result = rt.condition(expr)
  (result, rt.trace)
}