///|
pub struct Program {
  syntax : Script
  capabilities : Array[String]
  limits : Limits
}

///|
priv struct Arguments {
  flags : Array[String]
  options : Map[String, String]
  values : Array[Argument]
}

///|
pub fn supported_capabilities() -> Array[String] {
  [
    "relational", "comparator-i;ascii-numeric", "subaddress", "variables", "copy",
    "fileinto", "envelope", "comparator-i;octet", "comparator-i;ascii-casemap",
  ]
}

///|
fn split_args(
  args : Array[Argument],
  span : Span,
) -> Arguments raise SieveError {
  let flags : Array[String] = []
  let options : Map[String, String] = Map([])
  let values : Array[Argument] = []
  let mut i = 0
  let mut positional = false
  while i < args.length() {
    match args[i] {
      Flag(name) => {
        if positional {
          fail(
            "check.tag_order", "tags must precede positional arguments", span,
          )
        }
        if flags.contains(name) {
          fail("check.duplicate_tag", "duplicate tag :\{name}", span)
        }
        flags.push(name)
        if name == "comparator" || name == "value" || name == "count" {
          i += 1
          if i >= args.length() {
            fail("check.tag_value", "tag requires a string value", span)
          }
          match args[i] {
            Literal(value) => options[name] = value
            _ =>
              fail("check.tag_value", "tag value must be a single string", span)
          }
        }
      }
      arg => {
        positional = true
        values.push(arg)
      }
    }
    i += 1
  }
  { flags, options, values }
}

///|
fn strings_arg(arg : Argument, span : Span) -> Array[String] raise SieveError {
  match arg {
    Literal(s) => [s]
    Strings(ss) => ss
    _ => {
      fail("check.string", "expected string or string list", span)
      []
    }
  }
}

///|
fn single_arg(arg : Argument, span : Span) -> String raise SieveError {
  match arg {
    Literal(s) => s
    _ => {
      fail("check.single", "expected a single string", span)
      ""
    }
  }
}

///|
fn count_args(a : Arguments, n : Int, span : Span) -> Unit raise SieveError {
  if a.values.length() != n {
    fail("check.arity", "expected \{n} positional arguments", span)
  }
}

///|
fn allowed_flags(
  a : Arguments,
  allowed : Array[String],
  span : Span,
) -> Unit raise SieveError {
  for flag in a.flags {
    if !allowed.contains(flag) {
      fail("check.tag", "unsupported tag :\{flag}", span)
    }
  }
}

///|
fn exclusive(
  a : Arguments,
  group : Array[String],
  span : Span,
) -> Unit raise SieveError {
  let mut count = 0
  for flag in group {
    if a.flags.contains(flag) {
      count += 1
    }
  }
  if count > 1 {
    fail("check.conflicting_tags", "mutually exclusive tags", span)
  }
}

///|
fn needs(
  caps : Array[String],
  name : String,
  span : Span,
) -> Unit raise SieveError {
  if !caps.contains(name) {
    fail("check.require", "missing require \"\{name}\"", span)
  }
}

///|
fn check_match(a : Arguments, span : Span) -> Unit raise SieveError {
  exclusive(a, ["is", "contains", "matches", "value", "count"], span)
  match a.options.get("comparator") {
    Some("i;octet" | "i;ascii-casemap" | "i;ascii-numeric") | None => ()
    _ => fail("check.comparator", "unsupported comparator", span)
  }
}

///|
fn check_test(expr : Test, caps : Array[String]) -> Unit raise SieveError {
  match expr {
    Not(child, _) => check_test(child, caps)
    AnyOf(children, _) | AllOf(children, _) =>
      for child in children {
        check_test(child, caps)
      }
    Call(name, args, span) => {
      let a = split_args(args, span)
      match name {
        "true" | "false" => {
          allowed_flags(a, [], span)
          count_args(a, 0, span)
        }
        "exists" => {
          allowed_flags(a, [], span)
          count_args(a, 1, span)
          ignore(strings_arg(a.values[0], span))
        }
        "size" => {
          allowed_flags(a, ["over", "under"], span)
          count_args(a, 1, span)
          if a.flags.length() != 1 {
            fail("check.size", "size requires :over or :under", span)
          }
          match a.values[0] {
            Quantity(_) => ()
            _ => fail("check.number", "size requires a number", span)
          }
        }
        "string" => {
          needs(caps, "variables", span)
          allowed_flags(
            a,
            ["is", "contains", "matches", "comparator", "value", "count"],
            span,
          )
          count_args(a, 2, span)
          check_match(a, span)
          check_relational(a, caps, span)
          ignore(strings_arg(a.values[0], span))
          ignore(strings_arg(a.values[1], span))
        }
        "header" | "address" | "envelope" => {
          let flags = [
            "is", "contains", "matches", "comparator", "value", "count",
          ]
          if name != "header" {
            flags.push("user")
            flags.push("detail")
            if a.flags.contains("user") || a.flags.contains("detail") {
              needs(caps, "subaddress", span)
            }
            flags.push("all")
            flags.push("localpart")
            flags.push("domain")
            exclusive(a, ["all", "localpart", "domain", "user", "detail"], span)
          }
          if name == "envelope" {
            needs(caps, "envelope", span)
          }
          allowed_flags(a, flags, span)
          count_args(a, 2, span)
          check_match(a, span)
          check_relational(a, caps, span)
          let fields = strings_arg(a.values[0], span)
          ignore(strings_arg(a.values[1], span))
          if name == "envelope" {
            for field in fields {
              if ascii_lower(field) != "from" && ascii_lower(field) != "to" {
                fail(
                  "check.envelope", "only from/to envelope fields are supported",
                  span,
                )
              }
            }
          }
        }
        _ => fail("check.test", "unknown or unsupported test: \{name}", span)
      }
    }
  }
}

///|
fn check_statements(
  body : Array[Statement],
  caps : Array[String],
  top : Bool,
) -> Unit raise SieveError {
  let mut executable = false
  for statement in body {
    match statement {
      Command(name, args, span) => {
        let a = split_args(args, span)
        if name == "set" {
          check_set(a, caps, span)
        } else if name == "fileinto" || name == "redirect" {
          allowed_flags(a, ["copy"], span)
          if a.flags.contains("copy") {
            needs(caps, "copy", span)
          }
        } else {
          allowed_flags(a, [], span)
        }
        match name {
          "require" => {
            if !top || executable {
              fail(
                "check.require_order", "require must precede executable commands at top level",
                span,
              )
            }
            count_args(a, 1, span)
            for cap in strings_arg(a.values[0], span) {
              if !supported_capabilities().contains(cap) {
                fail("check.capability", "unsupported capability: \{cap}", span)
              }
              if !caps.contains(cap) {
                caps.push(cap)
              }
            }
          }
          "set" => executable = true
          "keep" | "discard" | "stop" => {
            executable = true
            count_args(a, 0, span)
          }
          "fileinto" | "redirect" => {
            executable = true
            count_args(a, 1, span)
            let destination = single_arg(a.values[0], span)
            if destination.is_empty() ||
              destination.contains_char('\r') ||
              destination.contains_char('\n') {
              fail(
                "check.destination", "destination must be nonempty and single-line",
                span,
              )
            }
            if name == "fileinto" {
              needs(caps, "fileinto", span)
            }
          }
          _ =>
            fail(
              "check.command",
              "unknown or unsupported command: \{name}",
              span,
            )
        }
      }
      Branch(branches, otherwise, _) => {
        executable = true
        for branch in branches {
          check_test(branch.condition, caps)
          check_statements(branch.body, caps, false)
        }
        check_statements(otherwise, caps, false)
      }
    }
  }
}

///|
pub fn compile(
  source : String,
  limits? : Limits = Limits::default(),
) -> Program raise SieveError {
  let syntax = parse(source, limits~)
  let capabilities : Array[String] = []
  check_statements(syntax.statements, capabilities, true)
  { syntax, capabilities, limits }
}

///|
pub fn Program::required_capabilities(self : Program) -> Array[String] {
  self.capabilities.copy()
}