///|
/// Action taken when a CIDR rule matches.
pub(all) enum RuleAction {
  Allow
  Deny
} derive(Eq, Debug)

///|
pub fn RuleAction::label(self : RuleAction) -> String {
  match self {
    Allow => "allow"
    Deny => "deny"
  }
}

///|
pub fn RuleAction::parse(input : String) -> Result[RuleAction, String] {
  let text = lower_ascii(trim_ascii(input))
  match text {
    "allow" | "permit" | "accept" => Ok(Allow)
    "deny" | "block" | "reject" => Ok(Deny)
    _ => Err("unknown rule action: " + input)
  }
}

///|
/// A single ordered firewall, proxy, or gateway rule.
pub struct Rule {
  id : String
  action : RuleAction
  block : CidrBlock
  source_block : String
  note : String
} derive(Eq, Debug)

///|
pub fn Rule::new(
  id : String,
  action : RuleAction,
  block : CidrBlock,
  source_block? : String = block.to_string(),
  note? : String = "",
) -> Rule {
  { id, action, block, source_block, note }
}

///|
pub fn Rule::id(self : Rule) -> String {
  self.id
}

///|
pub fn Rule::action(self : Rule) -> RuleAction {
  self.action
}

///|
pub fn Rule::block(self : Rule) -> CidrBlock {
  self.block
}

///|
pub fn Rule::source_block(self : Rule) -> String {
  self.source_block
}

///|
pub fn Rule::is_canonical(self : Rule) -> Bool {
  self.source_block == self.block.to_string()
}

///|
pub fn Rule::note(self : Rule) -> String {
  self.note
}

///|
pub fn Rule::matches(self : Rule, ip : IPv4) -> Bool {
  self.block.contains_ip(ip)
}

///|
pub fn Rule::same_decision(self : Rule, other : Rule) -> Bool {
  self.action == other.action
}

///|
pub fn Rule::summary(self : Rule) -> String {
  if self.note == "" {
    self.id + ": " + self.action.label() + " " + self.block.to_string()
  } else {
    self.id +
    ": " +
    self.action.label() +
    " " +
    self.block.to_string() +
    " # " +
    self.note
  }
}

///|
pub fn Rule::parse(id : String, line : String) -> Result[Rule, String] {
  let clean = strip_comment(line)
  let tokens = split_ascii_words(clean)
  guard tokens.length() >= 2 else {
    return Err("rule line must contain an action and a CIDR block: " + line)
  }
  match RuleAction::parse(tokens[0]) {
    Err(err) => Err(err)
    Ok(action) =>
      match CidrBlock::parse(tokens[1]) {
        Err(err) => Err(err)
        Ok(block) => {
          let mut note = ""
          if tokens.length() > 2 {
            note = join_words(tokens[2:])
          }
          Ok(Rule::new(id, action, block, source_block=tokens[1], note~))
        }
      }
  }
}

///|
fn strip_comment(line : String) -> String {
  let chars = line.to_array()
  let mut end = chars.length()
  for index = 0; index < chars.length(); index = index + 1 {
    if chars[index] == '#' {
      end = index
      break
    }
  }
  trim_ascii(String::from_array(chars[0:end]))
}

///|
fn split_ascii_words(input : String) -> Array[String] {
  let chars = input.to_array()
  let words : Array[String] = []
  let mut start = -1
  for index = 0; index < chars.length(); index = index + 1 {
    if is_ascii_space(chars[index]) {
      if start != -1 {
        words.push(String::from_array(chars[start:index]))
        start = -1
      }
    } else if start == -1 {
      start = index
    }
  }
  if start != -1 {
    words.push(String::from_array(chars[start:]))
  }
  words
}

///|
fn join_words(words : ArrayView[String]) -> String {
  let mut output = ""
  for index = 0; index < words.length(); index = index + 1 {
    if index > 0 {
      output = output + " "
    }
    output = output + words[index]
  }
  output
}

///|
fn lower_ascii(input : String) -> String {
  let chars = input.to_array()
  let output : Array[Char] = []
  for char in chars {
    let code = char.to_int()
    if code >= 'A' && code <= 'Z' {
      output.push((code + 32).unsafe_to_char())
    } else {
      output.push(char)
    }
  }
  String::from_array(output)
}