///|
pub enum AclParseKind {
  EmptyLine
  UnknownDecision
  UnknownTarget
  MissingField
  InvalidAddress
  InvalidNetwork
  InvalidRange
} derive(Eq, Debug)

///|
pub struct AclParseError {
  line : Int
  input : String
  kind : AclParseKind
} derive(Eq, Debug)

///|
pub struct AclBatchReport {
  allowed : Int
  denied : Int
  lines : Array[String]
} derive(Debug)

///|
pub fn AclParseError::to_string(self : AclParseError) -> String {
  "line \{self.line}: \{self.kind.to_string()} in '\{self.input}'"
}

///|
pub fn AclParseKind::to_string(self : AclParseKind) -> String {
  match self {
    EmptyLine => "empty line"
    UnknownDecision => "unknown decision"
    UnknownTarget => "unknown target"
    MissingField => "missing field"
    InvalidAddress => "invalid address"
    InvalidNetwork => "invalid network"
    InvalidRange => "invalid range"
  }
}

///|
pub fn AclRule::parse_line(line : String) -> Result[AclRule, AclParseError] {
  parse_acl_line_at(line, 1)
}

///|
pub fn AclPolicy::parse_lines(
  lines : Array[String],
  default_decision : AclDecision,
) -> Result[AclPolicy, AclParseError] {
  let mut policy = AclPolicy::new(default_decision)
  let mut line_no = 1
  for line in lines {
    let trimmed = trim_ascii(line)
    if trimmed.length() > 0 && !starts_with(trimmed, "#") {
      match parse_acl_line_at(trimmed, line_no) {
        Err(error) => return Err(error)
        Ok(rule) => policy = policy.push(rule)
      }
    }
    line_no = line_no + 1
  }
  Ok(policy)
}

///|
pub fn AclPolicy::evaluate_many(
  self : AclPolicy,
  addresses : Array[Ipv4Address],
) -> AclBatchReport {
  let lines = Array::new()
  let mut allowed = 0
  let mut denied = 0
  for address in addresses {
    let evaluation = self.evaluate(address)
    if evaluation.decision() == Allow {
      allowed = allowed + 1
    } else {
      denied = denied + 1
    }
    lines.push(evaluation.to_string())
  }
  { allowed, denied, lines }
}

///|
pub fn AclEvaluation::decision(self : AclEvaluation) -> AclDecision {
  self.decision
}

///|
pub fn AclEvaluation::matched_rule(self : AclEvaluation) -> String? {
  self.matched_rule
}

///|
pub fn AclBatchReport::allowed(self : AclBatchReport) -> Int {
  self.allowed
}

///|
pub fn AclBatchReport::denied(self : AclBatchReport) -> Int {
  self.denied
}

///|
pub fn AclBatchReport::lines(self : AclBatchReport) -> Array[String] {
  self.lines.copy()
}

///|
pub fn AclBatchReport::to_string(self : AclBatchReport) -> String {
  let mut text = "allowed=\{self.allowed}, denied=\{self.denied}"
  for line in self.lines {
    text = text + "\n" + line
  }
  text
}

///|
fn parse_acl_line_at(
  line : String,
  line_no : Int,
) -> Result[AclRule, AclParseError] {
  let parts = split_words(line)
  if parts.length() == 0 {
    return Err({ line: line_no, input: line, kind: EmptyLine })
  }
  if parts.length() < 3 {
    return Err({ line: line_no, input: line, kind: MissingField })
  }
  let decision = match parse_acl_decision(parts[0]) {
    None => return Err({ line: line_no, input: line, kind: UnknownDecision })
    Some(value) => value
  }
  let target_kind = parts[1]
  let target_value = parts[2]
  let name = if parts.length() >= 4 {
    join_words(parts, 3)
  } else {
    "\{parts[0]}-\{parts[1]}-\{parts[2]}"
  }
  match target_kind {
    "net" | "network" | "cidr" =>
      match Ipv4Network::parse(target_value) {
        Err(_) => Err({ line: line_no, input: line, kind: InvalidNetwork })
        Ok(network) => Ok(AclRule::network(name, decision, network))
      }
    "addr" | "address" | "host" =>
      match Ipv4Address::parse(target_value) {
        Err(_) => Err({ line: line_no, input: line, kind: InvalidAddress })
        Ok(address) => Ok(AclRule::address(name, decision, address))
      }
    "range" =>
      if parts.length() < 4 {
        Err({ line: line_no, input: line, kind: MissingField })
      } else {
        let range_name = if parts.length() >= 5 {
          join_words(parts, 4)
        } else {
          "\{parts[0]}-range"
        }
        match Ipv4Range::parse(parts[2], parts[3]) {
          Err(_) => Err({ line: line_no, input: line, kind: InvalidRange })
          Ok(range) => Ok(AclRule::range(range_name, decision, range))
        }
      }
    _ => Err({ line: line_no, input: line, kind: UnknownTarget })
  }
}

///|
fn parse_acl_decision(text : String) -> AclDecision? {
  match text {
    "allow" | "permit" | "accept" => Some(Allow)
    "deny" | "reject" | "block" => Some(Deny)
    _ => None
  }
}

///|
fn split_words(input : String) -> Array[String] {
  let result = Array::new()
  let mut current = ""
  for ch in input {
    if ch == ' ' || ch == '\t' {
      if current.length() > 0 {
        result.push(current)
        current = ""
      }
    } else {
      current = current + ch.to_string()
    }
  }
  if current.length() > 0 {
    result.push(current)
  }
  result
}

///|
fn join_words(parts : Array[String], start : Int) -> String {
  let mut text = ""
  let mut index = start
  while index < parts.length() {
    if index > start {
      text = text + " "
    }
    text = text + parts[index]
    index = index + 1
  }
  text
}

///|
fn trim_ascii(input : String) -> String {
  trim_right_ascii(trim_left_ascii(input))
}

///|
fn trim_left_ascii(input : String) -> String {
  let mut seen = false
  let mut text = ""
  for ch in input {
    if !seen && (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') {
      ()
    } else {
      seen = true
      text = text + ch.to_string()
    }
  }
  text
}

///|
fn trim_right_ascii(input : String) -> String {
  let chars = Array::new()
  for ch in input {
    chars.push(ch)
  }
  let mut end = chars.length() - 1
  while end >= 0 &&
        (
          chars[end] == ' ' ||
          chars[end] == '\t' ||
          chars[end] == '\r' ||
          chars[end] == '\n'
        ) {
    end = end - 1
  }
  let mut text = ""
  let mut index = 0
  while index <= end {
    text = text + chars[index].to_string()
    index = index + 1
  }
  text
}

///|
fn starts_with(input : String, prefix : String) -> Bool {
  input.has_prefix(prefix)
}