///|
/// A reusable validation rule for measured equilibrium data.
pub(all) enum DataRule {
  XRange(lower~ : Double, upper~ : Double)
  YRange(lower~ : Double, upper~ : Double)
  XStrictlyIncreasing
  NoRejectedRows
  MaximumAbsoluteSlope(value~ : Double)
  MaximumRelativeJump(value~ : Double)
  MinimumRows(count~ : Int)
} derive(Debug, Eq, ToJson)

///|
pub(all) struct RuleResult {
  rule_name : String
  passed : Bool
  violations : Int
  first_row : Int
  message : String
} derive(Debug, Eq, ToJson)

///|
fn rule_name(rule : DataRule) -> String {
  match rule {
    XRange(_) => "x-range"
    YRange(_) => "y-range"
    XStrictlyIncreasing => "x-increasing"
    NoRejectedRows => "rejected-rows"
    MaximumAbsoluteSlope(_) => "maximum-slope"
    MaximumRelativeJump(_) => "maximum-relative-jump"
    MinimumRows(_) => "minimum-rows"
  }
}

///|
pub fn validate_rule(
  table : MeasurementTable,
  rule : DataRule,
) -> RuleResult raise @domain.ExtractionError {
  let rows = table.rows()
  let mut violations = 0
  let mut first = -1
  let mut message = "rule passed"
  match rule {
    XRange(lower~, upper~) => {
      @domain.require_finite("x rule lower", lower)
      @domain.require_finite("x rule upper", upper)
      for index, row in rows {
        if row.x < lower || row.x > upper {
          violations += 1
          if first < 0 {
            first = index
          }
        }
      }
      message = "x values must remain inside the declared range"
    }
    YRange(lower~, upper~) => {
      @domain.require_finite("y rule lower", lower)
      @domain.require_finite("y rule upper", upper)
      for index, row in rows {
        if row.y < lower || row.y > upper {
          violations += 1
          if first < 0 {
            first = index
          }
        }
      }
      message = "y values must remain inside the declared range"
    }
    XStrictlyIncreasing => {
      for index in 1.. {
      for index, row in rows {
        match row.quality {
          Rejected(_) => {
            violations += 1
            if first < 0 {
              first = index
            }
          }
          _ => ()
        }
      }
      message = "rejected measurements must be removed before fitting"
    }
    MaximumAbsoluteSlope(value~) => {
      @domain.require_positive("maximum slope", value)
      for index in 1.. value {
          violations += 1
          if first < 0 {
            first = index
          }
        }
      }
      message = "successive slope magnitude exceeds the configured limit"
    }
    MaximumRelativeJump(value~) => {
      @domain.require_positive("maximum relative jump", value)
      for index in 1.. value {
          violations += 1
          if first < 0 {
            first = index
          }
        }
      }
      message = "successive response change exceeds the configured limit"
    }
    MinimumRows(count~) => {
      if rows.length() < count {
        violations = count - rows.length()
        first = rows.length()
      }
      message = "table does not contain enough measurements"
    }
  }
  {
    rule_name: rule_name(rule),
    passed: violations == 0,
    violations,
    first_row: first,
    message,
  }
}

///|
pub fn validate_rules(
  table : MeasurementTable,
  rules : ArrayView[DataRule],
) -> Array[RuleResult] raise @domain.ExtractionError {
  [
    for rule in rules => validate_rule(table, rule)
  ]
}

///|
pub(all) struct QualityControlSummary {
  results : Array[RuleResult]
  passed : Bool
  total_violations : Int
  failed_rules : Int
} derive(Debug, Eq, ToJson)

///|
pub fn quality_control(
  table : MeasurementTable,
  rules : ArrayView[DataRule],
) -> QualityControlSummary raise @domain.ExtractionError {
  let results = validate_rules(table, rules)
  let mut violations = 0
  let mut failed = 0
  for result in results {
    violations += result.violations
    if !result.passed {
      failed += 1
    }
  }
  {
    results,
    passed: failed == 0,
    total_violations: violations,
    failed_rules: failed,
  }
}

///|
pub fn QualityControlSummary::to_markdown(
  self : QualityControlSummary,
) -> String {
  let lines = [
    "| Rule | Status | Violations | First row |", "|---|---|---:|---:|",
  ]
  for result in self.results {
    let status = if result.passed { "PASS" } else { "FAIL" }
    lines.push(
      "| \{result.rule_name} | \{status} | \{result.violations} | \{result.first_row} |",
    )
  }
  lines.push("")
  let overall = if self.passed { "PASS" } else { "FAIL" }
  lines.push("Overall: \{overall}")
  lines.join("\n")
}

///|
pub fn accepted_only(
  table : MeasurementTable,
) -> MeasurementTable raise @domain.ExtractionError {
  MeasurementTable::new(name=table.name(), rows=table.usable_rows())
}

///|
pub fn mark_quality_by_rules(
  table : MeasurementTable,
  rules : ArrayView[DataRule],
) -> MeasurementTable raise @domain.ExtractionError {
  let rows = table.rows()
  let marked = rows.copy()
  for index, row in rows {
    let mut is_suspect = false
    for rule in rules {
      let result = validate_rule(
        MeasurementTable::new(name="single", rows=[row]),
        rule,
      )
      if !result.passed {
        is_suspect = true
      }
    }
    if is_suspect {
      marked[index] = row.mark(Suspect(reason="quality-control rule failed"))
    }
  }
  MeasurementTable::new(name=table.name(), rows=marked)
}