///|
pub(all) struct ValidationIssue {
  code : String
  message : String
  value : Double
} derive(Debug, ToJson)

///|
pub(all) struct ValidationReport {
  valid : Bool
  issues : Array[ValidationIssue]
} derive(Debug, ToJson)

///|
fn valid_report() -> ValidationReport {
  { valid: true, issues: [] }
}

///|
fn invalid_report(
  code : String,
  message : String,
  value : Double,
) -> ValidationReport {
  { valid: false, issues: [{ code, message, value }] }
}

///|
pub fn validate_grid(cells : Int, length : Double) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  if cells < 2 {
    issues.push({
      code: "cells",
      message: "grid needs at least two cells",
      value: cells.to_double(),
    })
  }
  if length <= 0.0 {
    issues.push({
      code: "length",
      message: "domain length must be positive",
      value: length,
    })
  }
  { valid: issues.length() == 0, issues }
}

///|
pub fn validate_positive(value : Double, name : String) -> ValidationReport {
  if value > 0.0 {
    valid_report()
  } else {
    invalid_report(name, "value must be positive", value)
  }
}

///|
pub fn validate_non_negative(value : Double, name : String) -> ValidationReport {
  if value >= 0.0 {
    valid_report()
  } else {
    invalid_report(name, "value must not be negative", value)
  }
}

///|
pub fn validate_range(
  value : Double,
  low : Double,
  high : Double,
  name : String,
) -> ValidationReport {
  if low <= high && value >= low && value <= high {
    valid_report()
  } else {
    invalid_report(name, "value is outside the requested range", value)
  }
}

///|
pub fn validate_count(value : Int, name : String) -> ValidationReport {
  if value > 0 {
    valid_report()
  } else {
    invalid_report(name, "count must be positive", value.to_double())
  }
}

///|
pub fn validation_message(report : ValidationReport) -> String {
  if report.valid {
    "valid"
  } else if report.issues.length() == 0 {
    "invalid"
  } else {
    report.issues[0].message
  }
}

///|
pub fn clamp(value : Double, low : Double, high : Double) -> Double {
  if low > high {
    clamp(value, high, low)
  } else if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
pub fn clamp_int(value : Int, low : Int, high : Int) -> Int {
  if low > high {
    clamp_int(value, high, low)
  } else if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
pub fn lerp(a : Double, b : Double, t : Double) -> Double {
  a + (b - a) * t
}

///|
pub fn inverse_lerp(a : Double, b : Double, value : Double) -> Double {
  if a == b {
    0.0
  } else {
    (value - a) / (b - a)
  }
}

///|
pub fn modulo_index(index : Int, count : Int) -> Int {
  if count <= 0 {
    0
  } else {
    let remainder = index % count
    if remainder < 0 {
      remainder + count
    } else {
      remainder
    }
  }
}

///|
pub fn safe_ratio(
  numerator : Double,
  denominator : Double,
  fallback : Double,
) -> Double {
  if denominator == 0.0 {
    fallback
  } else {
    numerator / denominator
  }
}

///|
pub fn sum_values(values : ArrayView[Double]) -> Double {
  values.fold(init=0.0, fn(acc, value) { acc + value })
}

///|
pub fn weighted_sum(
  values : ArrayView[Double],
  weights : ArrayView[Double],
) -> Double {
  let count = if values.length() < weights.length() {
    values.length()
  } else {
    weights.length()
  }
  let mut total = 0.0
  for i in 0.. Double {
  let mut current = 0.0
  for value in values {
    if value.abs() > current {
      current = value.abs()
    }
  }
  current
}

///|
pub fn all_close(
  a : ArrayView[Double],
  b : ArrayView[Double],
  absolute : Double,
  relative : Double,
) -> Bool {
  if a.length() != b.length() {
    false
  } else {
    let mut result = true
    for i in 0.. absolute + relative * scale {
        result = false
      }
    }
    result
  }
}

///|
pub fn fill_like(values : ArrayView[Double], value : Double) -> Array[Double] {
  Array::make(values.length(), value)
}

///|
pub fn copy_values(values : ArrayView[Double]) -> Array[Double] {
  values.to_owned()
}

///|
pub fn approximate_log(value : Double) -> Double {
  if value <= 0.0 {
    0.0
  } else {
    let mut reduced = value
    let mut exponent = 0
    while reduced > 2.0 {
      reduced = reduced * 0.5
      exponent = exponent + 1
    }
    while reduced < 1.0 {
      reduced = reduced * 2.0
      exponent = exponent - 1
    }
    let x = (reduced - 1.0) / (reduced + 1.0)
    let x2 = x * x
    let mut power = x
    let mut total = 0.0
    let mut denominator = 1.0
    for _ in 0..<12 {
      total = total + power / denominator
      power = power * x2
      denominator = denominator + 2.0
    }
    2.0 * total + exponent.to_double() * 0.6931471805599453
  }
}