///|
fn variable_name(name : String) -> Bool {
  let chars = name.to_array()
  !chars.is_empty() && name_start(chars[0]) && chars.all(name_char)
}

///|
fn Runtime::expand(
  self : Runtime,
  text : String,
  span : Span,
) -> String raise SieveError {
  if !self.variables_enabled {
    return text
  }
  let chars = text.to_array()
  let out = StringBuilder()
  let mut size = 0
  let mut i = 0
  while i < chars.length() {
    self.budget.step(span)
    if chars[i] == '$' && i + 1 < chars.length() && chars[i + 1] == '{' {
      let mut end = i + 2
      while end < chars.length() && chars[end] != '}' {
        self.budget.step(span)
        end += 1
      }
      if end < chars.length() {
        let name = chars_text(chars, i + 2, end)
        let numeric = !name.is_empty() && name.all(c => c.is_ascii_digit())
        if variable_name(name) || numeric {
          let value = if numeric {
            let mut index = 0
            for c in name {
              index = index * 10 + c.to_int() - 48
              if index > 9 {
                fail(
                  "variable.capture", "only match variables 0 through 9 are supported",
                  span,
                )
              }
            }
            self.captures[index]
          } else {
            self.variables.get(ascii_lower(name)).unwrap_or("")
          }
          size += value.length()
          if size > self.budget.limits.string_chars {
            fail("limit.expansion", "expanded string too long", span)
          }
          out.write_string(value)
          i = end + 1
          continue
        }
      }
    }
    size += chars[i].utf16_len()
    if size > self.budget.limits.string_chars {
      fail("limit.expansion", "expanded string too long", span)
    }
    out.write_char(chars[i])
    i += 1
  }
  out.to_string()
}

///|
fn Runtime::expanded_strings(
  self : Runtime,
  arg : Argument,
  span : Span,
) -> Array[String] raise SieveError {
  let out : Array[String] = []
  for text in strings_arg(arg, span) {
    out.push(self.expand(text, span))
  }
  out
}

///|
fn modify_variable(value : String, flags : Array[String]) -> String {
  let mut result = value
  if flags.contains("lower") {
    result = result.to_lower()
  }
  if flags.contains("upper") {
    result = result.to_upper()
  }
  if flags.contains("lowerfirst") || flags.contains("upperfirst") {
    let chars = result.to_array()
    if !chars.is_empty() {
      let first = chars_text(chars, 0, 1)
      result = (if flags.contains("lowerfirst") {
          first.to_lower()
        } else {
          first.to_upper()
        }) +
        chars_text(chars, 1, chars.length())
    }
  }
  if flags.contains("quotewildcard") {
    let out = StringBuilder()
    for c in result {
      if c == '*' || c == '?' || c == '\\' {
        out.write_char('\\')
      }
      out.write_char(c)
    }
    result = out.to_string()
  }
  if flags.contains("length") {
    result = result.char_length().to_string()
  }
  result
}

///|
fn check_set(
  a : Arguments,
  caps : Array[String],
  span : Span,
) -> Unit raise SieveError {
  needs(caps, "variables", span)
  allowed_flags(
    a,
    ["lower", "upper", "lowerfirst", "upperfirst", "quotewildcard", "length"],
    span,
  )
  exclusive(a, ["lower", "upper"], span)
  exclusive(a, ["lowerfirst", "upperfirst"], span)
  count_args(a, 2, span)
  if !variable_name(single_arg(a.values[0], span)) {
    fail(
      "check.variable", "set target must be an identifier, not a match variable",
      span,
    )
  }
  ignore(single_arg(a.values[1], span))
}

///|
fn Runtime::set_variable(
  self : Runtime,
  a : Arguments,
  span : Span,
) -> Unit raise SieveError {
  let name = ascii_lower(single_arg(a.values[0], span))
  let value = modify_variable(
    self.expand(single_arg(a.values[1], span), span),
    a.flags,
  )
  if value.length() > self.budget.limits.string_chars {
    fail("limit.expansion", "variable value exceeds limit", span)
  }
  if !self.variables.contains(name) &&
    self.variables.length() >= self.budget.limits.variables {
    fail("limit.variables", "variable count exhausted", span)
  }
  self.variables[name] = value
  self.record("variable", name, span, None)
}

///|
fn Runtime::matching(
  self : Runtime,
  value : String,
  key : String,
  a : Arguments,
  span : Span,
) -> Bool raise SieveError {
  if !self.variables_enabled || mode_from(a) != Matches {
    return compare_text(
      value,
      key,
      mode_from(a),
      comparator_from(a),
      self.budget,
      span,
    )
  }
  if value.length() > self.budget.limits.string_chars ||
    key.length() > self.budget.limits.string_chars {
    fail("limit.string", "matching input too long", span)
  }
  let text = value.to_array()
  let units = glob_units(key, comparator_from(a))
  let width = text.length() + 1
  if units.length() + 1 >
    (self.budget.limits.comparisons - self.budget.comparisons) / width {
    fail("limit.comparisons", "capture table exceeds matching budget", span)
  }
  self.budget.compare(width * (units.length() + 1), span)
  self.budget.step(span)
  let table = Array::make(width * (units.length() + 1), false)
  table[units.length() * width + text.length()] = true
  let mut row = units.length() - 1
  while row >= 0 {
    let mut col = text.length()
    while col >= 0 {
      table[row * width + col] = match units[row] {
        Many =>
          table[(row + 1) * width + col] ||
          (col < text.length() && table[row * width + col + 1])
        One => col < text.length() && table[(row + 1) * width + col + 1]
        Exact(c) =>
          col < text.length() &&
          fold_char(text[col], comparator_from(a)) == c &&
          table[(row + 1) * width + col + 1]
      }
      col -= 1
    }
    row -= 1
  }
  if !table[0] {
    return false
  }
  for n = 0; n <= 9; n = n + 1 {
    self.captures[n] = ""
  }
  self.captures[0] = value
  let mut pos = 0
  let mut capture = 1
  for i = 0; i < units.length(); i = i + 1 {
    match units[i] {
      Exact(_) => pos += 1
      One => {
        if capture <= 9 {
          self.captures[capture] = chars_text(text, pos, pos + 1)
        }
        capture += 1
        pos += 1
      }
      Many => {
        let mut end = text.length()
        while end > pos && !table[(i + 1) * width + end] {
          end -= 1
        }
        if capture <= 9 {
          self.captures[capture] = chars_text(text, pos, end)
        }
        capture += 1
        pos = end
      }
    }
  }
  true
}