///|
/// Invalid profiles and geometry are recoverable errors, including direct library input.
pub(all) suberror ValidationError {
  InvalidWeight(String)
  InvalidTree(String)
  InvalidGeometry(String)
}

///|
fn finite(value : Double) -> Bool {
  !value.is_nan() && !value.is_inf()
}

///|
fn tolerance(value : Double) -> Double {
  1.0e-12 * larger(1.0, value.abs())
}

///|
fn check_weight(value : Double, where_ : String) -> Unit raise ValidationError {
  if !finite(value) || value < 0.0 {
    raise InvalidWeight("\{where_}: weight must be finite and nonnegative")
  }
}

///|
/// Validate iteratively so that unlimited-depth profiles do not consume the host stack.
pub fn validate_tree(tree : CallTree) -> Unit raise ValidationError {
  check_weight(tree.total, "total")
  let pending : Array[Node] = []
  let mut sum = 0.0
  let root_names : Map[String, Bool] = Map([])
  for root in tree.roots {
    if root_names.contains(root.name) {
      raise InvalidTree("duplicate root name")
    }
    root_names.set(root.name, true)
    sum = sum + root.value
    pending.push(root)
  }
  if !finite(sum) || (sum - tree.total).abs() > tolerance(tree.total) {
    raise InvalidTree("root weights do not match total")
  }
  while pending.length() > 0 {
    let node = pending[pending.length() - 1]
    ignore(pending.pop())
    check_weight(node.value, node.name)
    if node.name == "" {
      raise InvalidTree("empty frame name")
    }
    let names : Map[String, Bool] = Map([])
    let mut children = 0.0
    for child in node.children {
      if names.contains(child.name) {
        raise InvalidTree("duplicate child name")
      }
      names.set(child.name, true)
      check_weight(child.value, child.name)
      children = children + child.value
      pending.push(child)
    }
    if !finite(children) || children - node.value > tolerance(node.value) {
      raise InvalidTree("children exceed parent: " + node.name)
    }
    for delta in [node.delta, node.self_delta] {
      if delta is Some(value) && !finite(value) {
        raise InvalidTree("non-finite difference")
      }
    }
  }
}

///|
fn positive_geometry(
  value : Double,
  name : String,
) -> Unit raise ValidationError {
  if !finite(value) || value <= 0.0 {
    raise InvalidGeometry(name + " must be finite and positive")
  }
}

///|
fn larger(a : Double, b : Double) -> Double {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn smaller(a : Double, b : Double) -> Double {
  if a < b {
    a
  } else {
    b
  }
}

///|
/// Validate independently supplied layouts before serializing any SVG.
fn validate_layout(layout : Layout) -> Unit raise ValidationError {
  positive_geometry(layout.width, "width")
  positive_geometry(layout.row_height, "row_height")
  check_weight(layout.total, "layout total")
  check_weight(layout.max_delta, "max_delta")
  if layout.max_depth < 0 {
    raise InvalidGeometry("negative depth")
  }
  let path : Array[Int] = []
  let covered : Array[Double] = Array::make(layout.rects.length(), 0.0)
  let next_x : Array[Double] = Array::make(layout.rects.length(), 0.0)
  let mut root_right = 0.0
  let mut actual_depth = 0
  let mut root_value = 0.0
  for i = 0; i < layout.rects.length(); i = i + 1 {
    let r = layout.rects[i]
    check_weight(r.value, r.name)
    if !finite(r.x) ||
      !finite(r.y) ||
      !finite(r.width) ||
      !finite(r.height) ||
      r.x < 0.0 ||
      r.y < 0.0 ||
      r.width < 0.0 ||
      r.height < 0.0 ||
      r.depth < 0 ||
      !finite(r.x + r.width) ||
      r.x + r.width > layout.width + tolerance(layout.width) {
      raise InvalidGeometry("invalid rectangle: " + r.name)
    }
    if r.delta is Some(d) &&
      (!finite(d) || d.abs() > layout.max_delta + tolerance(layout.max_delta)) {
      raise InvalidGeometry("invalid delta scale")
    }
    let expected_y = (if layout.inverted {
        r.depth
      } else {
        layout.max_depth - r.depth
      }).to_double() *
      layout.row_height
    if r.depth > layout.max_depth ||
      (r.y - expected_y).abs() > tolerance(expected_y) ||
      (r.height - layout.row_height).abs() > tolerance(layout.row_height) {
      raise InvalidGeometry("invalid row geometry")
    }
    actual_depth = if r.depth > actual_depth { r.depth } else { actual_depth }
    while path.length() > 0 &&
          layout.rects[path[path.length() - 1]].depth >= r.depth {
      ignore(path.pop())
    }
    if r.depth == 0 {
      if (r.x - root_right).abs() > tolerance(layout.width) {
        raise InvalidGeometry("root gap or overlap")
      }
      root_right = r.x + r.width
      root_value = root_value + r.value
    } else {
      guard path.length() > 0 else { raise InvalidGeometry("missing parent") }
      let idx = path[path.length() - 1]
      let parent = layout.rects[idx]
      let expected_x = if covered[idx] == 0.0 { parent.x } else { next_x[idx] }
      if r.depth != parent.depth + 1 ||
        (r.x - expected_x).abs() > tolerance(layout.width) ||
        r.x + r.width > parent.x + parent.width + tolerance(layout.width) {
        raise InvalidGeometry("child gap, overlap or overflow")
      }
      covered[idx] = covered[idx] + r.width
      next_x[idx] = r.x + r.width
    }
    path.push(i)
  }
  for i = 0; i < covered.length(); i = i + 1 {
    if covered[i] > 0.0 &&
      (covered[i] - layout.rects[i].width).abs() > tolerance(layout.width) {
      raise InvalidGeometry("incomplete child coverage")
    }
  }
  if actual_depth != layout.max_depth ||
    (root_value - layout.total).abs() > tolerance(layout.total) ||
    (
      layout.rects.length() > 0 &&
      (root_right - layout.width).abs() > tolerance(layout.width)
    ) {
    raise InvalidGeometry("inconsistent layout totals")
  }
}