///|
pub enum RuleOperator {
  GreaterThan
  LessThan
  EqualTo
  AtLeast
  AtMost
} derive(Debug, Eq)

///|
pub fn greater_than() -> RuleOperator {
  GreaterThan
}

///|
pub fn less_than() -> RuleOperator {
  LessThan
}

///|
pub fn equal_to() -> RuleOperator {
  EqualTo
}

///|
pub enum RuleSeverity {
  RuleInfo
  RuleWarning
  RuleError
} derive(Debug, Eq)

///|
pub fn rule_warning() -> RuleSeverity {
  RuleWarning
}

///|
pub fn rule_error() -> RuleSeverity {
  RuleError
}

///|
pub fn rule_info() -> RuleSeverity {
  RuleInfo
}

///|
pub struct Rule {
  code : String
  operator : RuleOperator
  field : String
  limit : Float
  severity : RuleSeverity
} derive(Debug, Eq)

///|
pub fn rule(
  code : String,
  operator : RuleOperator,
  field : String,
  limit : Float,
  severity : RuleSeverity,
) -> Rule {
  { code, operator, field, limit, severity }
}

///|
pub struct RuleValue {
  field : String
  value : Float
} derive(Debug, Eq)

///|
pub fn rule_value(field : String, value : Float) -> RuleValue {
  { field, value }
}

///|
pub struct RuleViolation {
  code : String
  field : String
  actual : Float
  limit : Float
  severity : RuleSeverity
  message : String
} derive(Debug, Eq)

///|
pub struct RuleEvaluation {
  passed : Bool
  violations : Array[RuleViolation]
  checked : Int
} derive(Debug, Eq)

///|
fn rule_value_for(values : Array[RuleValue], field : String) -> Float? {
  for value in values {
    if value.field == field {
      return Some(value.value)
    }
  }
  None
}

///|
fn rule_passes(operator : RuleOperator, actual : Float, limit : Float) -> Bool {
  match operator {
    GreaterThan => actual > limit
    LessThan => actual < limit
    EqualTo => (actual - limit).abs() < 0.000001
    AtLeast => actual >= limit
    AtMost => actual <= limit
  }
}

///|
fn rule_message(
  operator : RuleOperator,
  field : String,
  actual : Float,
  limit : Float,
) -> String {
  let operation = match operator {
    GreaterThan => ">"
    LessThan => "<"
    EqualTo => "="
    AtLeast => ">="
    AtMost => "<="
  }
  field +
  "=" +
  actual.to_string() +
  " must satisfy " +
  operation +
  " " +
  limit.to_string()
}

///|
pub fn evaluate_rules(
  rules : Array[Rule],
  values : Array[RuleValue],
) -> RuleEvaluation {
  let violations : Array[RuleViolation] = []
  let mut checked = 0
  for item in rules {
    match rule_value_for(values, item.field) {
      Some(actual) => {
        checked = checked + 1
        if !rule_passes(item.operator, actual, item.limit) {
          violations.push({
            code: item.code,
            field: item.field,
            actual,
            limit: item.limit,
            severity: item.severity,
            message: rule_message(item.operator, item.field, actual, item.limit),
          })
        }
      }
      None =>
        violations.push({
          code: item.code,
          field: item.field,
          actual: 0.0,
          limit: item.limit,
          severity: item.severity,
          message: item.field + " is missing",
        })
    }
  }
  { passed: violations.length() == 0, violations, checked }
}

///|
pub fn RuleEvaluation::error_count(self : RuleEvaluation) -> Int {
  let mut count = 0
  for violation in self.violations {
    if violation.severity is RuleError {
      count = count + 1
    }
  }
  count
}

///|
pub fn RuleEvaluation::warning_count(self : RuleEvaluation) -> Int {
  let mut count = 0
  for violation in self.violations {
    if violation.severity is RuleWarning {
      count = count + 1
    }
  }
  count
}

///|
pub fn RuleEvaluation::to_table(self : RuleEvaluation) -> ReportTable {
  let rows : Array[Array[String]] = []
  for violation in self.violations {
    rows.push([
      violation.code,
      violation.field,
      "{violation.actual}",
      "{violation.limit}",
      violation.message,
    ])
  }
  table(["code", "field", "actual", "limit", "message"], rows)
}

///|
pub fn RuleEvaluation::to_diagnostics(
  self : RuleEvaluation,
) -> Array[Diagnostic] {
  let result : Array[Diagnostic] = []
  for violation in self.violations {
    let severity = match violation.severity {
      RuleInfo => DiagHint
      RuleWarning => DiagWarning
      RuleError => DiagError
    }
    result.push(
      diagnostic(
        violation.code,
        severity,
        violation.field,
        violation.message,
        "review the input and calculation assumptions",
      ),
    )
  }
  result
}

///|
pub fn rule_gate(rules : Array[Rule], values : Array[RuleValue]) -> Bool {
  evaluate_rules(rules, values).passed
}

///|
pub fn merge_rules(left : Array[Rule], right : Array[Rule]) -> Array[Rule] {
  let result = left.copy()
  for item in right {
    result.push(item)
  }
  result
}

///|
pub fn rule_codes(rules : Array[Rule]) -> Array[String] {
  rules.map(fn(item) { item.code })
}

///|
pub fn rule_fields(rules : Array[Rule]) -> Array[String] {
  rules.map(fn(item) { item.field })
}

///|
pub fn rule_summary(evaluation : RuleEvaluation) -> String {
  "checked=" +
  evaluation.checked.to_string() +
  ", violations=" +
  evaluation.violations.length().to_string() +
  ", errors=" +
  evaluation.error_count().to_string()
}

///|
pub fn positive_rule(
  code : String,
  field : String,
  severity : RuleSeverity,
) -> Rule {
  rule(code, GreaterThan, field, 0.0, severity)
}

///|
pub fn non_negative_rule(
  code : String,
  field : String,
  severity : RuleSeverity,
) -> Rule {
  rule(code, AtLeast, field, 0.0, severity)
}

///|
pub fn upper_bound_rule(
  code : String,
  field : String,
  limit : Float,
  severity : RuleSeverity,
) -> Rule {
  rule(code, AtMost, field, limit, severity)
}

///|
pub fn lower_bound_rule(
  code : String,
  field : String,
  limit : Float,
  severity : RuleSeverity,
) -> Rule {
  rule(code, AtLeast, field, limit, severity)
}