///|
pub enum AclDecision {
  Allow
  Deny
} derive(Eq, Debug)

///|
pub enum AclTarget {
  Network(Ipv4Network)
  Range(Ipv4Range)
  Address(Ipv4Address)
} derive(Eq, Debug)

///|
pub struct AclRule {
  name : String
  decision : AclDecision
  target : AclTarget
} derive(Eq, Debug)

///|
pub struct AclPolicy {
  rules : Array[AclRule]
  default_decision : AclDecision
} derive(Debug)

///|
pub struct AclEvaluation {
  address : Ipv4Address
  decision : AclDecision
  matched_rule : String?
} derive(Eq, Debug)

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

///|
pub fn AclDecision::allow() -> AclDecision {
  Allow
}

///|
pub fn AclDecision::deny() -> AclDecision {
  Deny
}

///|
pub fn AclTarget::matches(self : AclTarget, address : Ipv4Address) -> Bool {
  match self {
    Network(network) => network.contains(address)
    Range(range) => range.contains(address)
    Address(exact) => exact == address
  }
}

///|
pub fn AclTarget::to_string(self : AclTarget) -> String {
  match self {
    Network(network) => network.to_string()
    Range(range) => range.to_string()
    Address(address) => address.to_string()
  }
}

///|
pub fn AclRule::network(
  name : String,
  decision : AclDecision,
  network : Ipv4Network,
) -> AclRule {
  { name, decision, target: Network(network) }
}

///|
pub fn AclRule::range(
  name : String,
  decision : AclDecision,
  range : Ipv4Range,
) -> AclRule {
  { name, decision, target: Range(range) }
}

///|
pub fn AclRule::address(
  name : String,
  decision : AclDecision,
  address : Ipv4Address,
) -> AclRule {
  { name, decision, target: Address(address) }
}

///|
pub fn AclRule::name(self : AclRule) -> String {
  self.name
}

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

///|
pub fn AclRule::target(self : AclRule) -> AclTarget {
  self.target
}

///|
pub fn AclRule::matches(self : AclRule, address : Ipv4Address) -> Bool {
  self.target.matches(address)
}

///|
pub fn AclRule::explain(self : AclRule) -> String {
  "\{self.decision.to_string()} \{self.target.to_string()} by \{self.name}"
}

///|
pub fn AclPolicy::new(default_decision : AclDecision) -> AclPolicy {
  { rules: Array::new(), default_decision }
}

///|
pub fn AclPolicy::allow_by_default() -> AclPolicy {
  AclPolicy::new(Allow)
}

///|
pub fn AclPolicy::deny_by_default() -> AclPolicy {
  AclPolicy::new(Deny)
}

///|
pub fn AclPolicy::push(self : AclPolicy, rule : AclRule) -> AclPolicy {
  let rules = self.rules.copy()
  rules.push(rule)
  { rules, default_decision: self.default_decision }
}

///|
pub fn AclPolicy::allow_network(
  self : AclPolicy,
  name : String,
  network : Ipv4Network,
) -> AclPolicy {
  self.push(AclRule::network(name, Allow, network))
}

///|
pub fn AclPolicy::deny_network(
  self : AclPolicy,
  name : String,
  network : Ipv4Network,
) -> AclPolicy {
  self.push(AclRule::network(name, Deny, network))
}

///|
pub fn AclPolicy::allow_range(
  self : AclPolicy,
  name : String,
  range : Ipv4Range,
) -> AclPolicy {
  self.push(AclRule::range(name, Allow, range))
}

///|
pub fn AclPolicy::deny_range(
  self : AclPolicy,
  name : String,
  range : Ipv4Range,
) -> AclPolicy {
  self.push(AclRule::range(name, Deny, range))
}

///|
pub fn AclPolicy::allow_address(
  self : AclPolicy,
  name : String,
  address : Ipv4Address,
) -> AclPolicy {
  self.push(AclRule::address(name, Allow, address))
}

///|
pub fn AclPolicy::deny_address(
  self : AclPolicy,
  name : String,
  address : Ipv4Address,
) -> AclPolicy {
  self.push(AclRule::address(name, Deny, address))
}

///|
pub fn AclPolicy::len(self : AclPolicy) -> Int {
  self.rules.length()
}

///|
pub fn AclPolicy::evaluate(
  self : AclPolicy,
  address : Ipv4Address,
) -> AclEvaluation {
  let mut index = 0
  while index < self.rules.length() {
    let rule = self.rules[index]
    if rule.matches(address) {
      return { address, decision: rule.decision, matched_rule: Some(rule.name) }
    }
    index = index + 1
  }
  { address, decision: self.default_decision, matched_rule: None }
}

///|
pub fn AclPolicy::allows(self : AclPolicy, address : Ipv4Address) -> Bool {
  self.evaluate(address).decision == Allow
}

///|
pub fn AclEvaluation::to_string(self : AclEvaluation) -> String {
  match self.matched_rule {
    Some(name) =>
      "\{self.address.to_string()} => \{self.decision.to_string()} by \{name}"
    None =>
      "\{self.address.to_string()} => \{self.decision.to_string()} by default"
  }
}