///|
pub(all) enum ComparisonPolicy {
  Exact
  Approximate(Double, Double)
}

///|
pub fn ComparisonPolicy::validate(self : ComparisonPolicy) -> Unit raise {
  match self {
    Exact => ()
    Approximate(absolute, relative) =>
      if !finite(absolute) ||
        !finite(relative) ||
        absolute < 0.0 ||
        relative < 0.0 {
        raise Invalid("execution.comparison_tolerance")
      }
  }
}

///|
pub fn compare(
  left : Value,
  right : Value,
  policy~ : ComparisonPolicy = Exact,
) -> Bool raise {
  policy.validate()
  validate_value(left, 0, Ref(100000))
  validate_value(right, 0, Ref(100000))
  compare_value(left, right, policy)
}

///|
fn compare_value(
  left : Value,
  right : Value,
  policy : ComparisonPolicy,
) -> Bool raise {
  fn arrays(a : Array[Value], b : Array[Value]) -> Bool raise {
    if a.length() != b.length() {
      return false
    }
    for i, v in a {
      if !compare_value(v, b[i], policy) {
        return false
      }
    }
    true
  }
  match (left, right) {
    (Decimal(a, ae), Decimal(b, be)) =>
      normalize_decimal(a, ae) == normalize_decimal(b, be)
    (Float64(a), Float64(b)) => {
      if a == b {
        return true
      }
      match policy {
        Exact => false
        Approximate(absolute, relative) => {
          let x = a.reinterpret_as_double()
          let y = b.reinterpret_as_double()
          // Non-finite bits and the sign of zero remain exact under either policy.
          if !finite(x) || !finite(y) || (x == 0.0 && y == 0.0) {
            return false
          }
          let distance = (x - y).abs()
          let tolerance = absolute +
            relative * (if x.abs() > y.abs() { x.abs() } else { y.abs() })
          finite(distance) && finite(tolerance) && distance <= tolerance
        }
      }
    }
    (Sequence(a), Sequence(b)) | (Product(a), Product(b)) => arrays(a, b)
    (Record(a), Record(b)) => {
      if a.length() != b.length() {
        return false
      }
      let fields = Map(b)
      for (name, v) in a {
        guard fields.get(name) is Some(other) else { return false }
        if !compare_value(v, other, policy) {
          return false
        }
      }
      true
    }
    (Constructor(owner, tag, a), Constructor(other_owner, other_tag, b)) =>
      owner == other_owner && tag == other_tag && arrays(a, b)
    (Optional(Some(a)), Optional(Some(b)))
    | (Outcome(Ok(a)), Outcome(Ok(b)))
    | (Outcome(Err(a)), Outcome(Err(b))) => compare_value(a, b, policy)
    _ => left == right
  }
}

///|
fn finite(value : Double) -> Bool {
  (value.reinterpret_as_uint64() & 0x7ff0000000000000UL) != 0x7ff0000000000000UL
}

///|
pub fn ComparisonPolicy::to_json(self : ComparisonPolicy) -> Json {
  match self {
    Exact => { "profile": "morphir-comparison-exact-v1" }
    Approximate(absolute, relative) =>
      {
        "profile": "morphir-comparison-approximate-v1",
        "absolute": absolute,
        "relative": relative,
      }
  }
}

///|
/// First mismatch as a JSON Pointer. Semantic comparisons retain rich typed values.
pub fn mismatch_path(
  left : Value,
  right : Value,
  policy~ : ComparisonPolicy = Exact,
) -> String? raise {
  if compare(left, right, policy~) {
    return None
  }
  locate_mismatch(left, right, policy, "")
}

///|
fn locate_mismatch(
  left : Value,
  right : Value,
  policy : ComparisonPolicy,
  path : String,
) -> String? raise {
  fn items(a : Array[Value], b : Array[Value]) -> String? raise {
    if a.length() != b.length() {
      return Some(path + "/length")
    }
    for i, value in a {
      if !compare_value(value, b[i], policy) {
        return locate_mismatch(value, b[i], policy, path + "/" + i.to_string())
      }
    }
    Some(path)
  }
  match (left, right) {
    (Sequence(a), Sequence(b)) | (Product(a), Product(b)) => items(a, b)
    (Record(a), Record(b)) => {
      let fields = Map(b)
      for (name, value) in a {
        let child = path +
          "/" +
          name.replace_all(old="~", new="~0").replace_all(old="/", new="~1")
        guard fields.get(name) is Some(other) else { return Some(child) }
        if !compare_value(value, other, policy) {
          return locate_mismatch(value, other, policy, child)
        }
      }
      Some(path)
    }
    (Constructor(owner, tag, a), Constructor(other_owner, other_tag, b)) =>
      if owner != other_owner || tag != other_tag {
        Some(path + "/constructor")
      } else {
        items(a, b)
      }
    (Optional(Some(a)), Optional(Some(b))) =>
      locate_mismatch(a, b, policy, path + "/just")
    (Outcome(Ok(a)), Outcome(Ok(b))) =>
      locate_mismatch(a, b, policy, path + "/ok")
    (Outcome(Err(a)), Outcome(Err(b))) =>
      locate_mismatch(a, b, policy, path + "/err")
    _ => Some(path)
  }
}