///|
// Bounded decimal indices. Never coerce booleans, signs or floating values.
fn checked_index(text : String, exclusive : Int) -> Int raise {
  guard text.length() > 0 && text.length() <= 10 else {
    raise Invalid("missing/oversized spreadsheet index")
  }
  let mut value = 0
  for c in text.code_units() {
    guard c >= '0' && c <= '9' && value <= 200000000 else {
      raise Invalid("invalid spreadsheet index")
    }
    value = value * 10 + c.to_int() - 48
  }
  guard value < exclusive else {
    raise Invalid("spreadsheet index out of range")
  }
  value
}

///|
fn validate_spreadsheet_values(pkg : Package) -> Unit raise {
  for kind in ["styles", "sharedStrings", "theme"] {
    guard pkg.audit.relationships
      .filter(fn(r) { r.kind == office_rel + kind })
      .length() <=
      1 else {
      raise Invalid("ambiguous spreadsheet supporting table")
    }
  }
  let mut strings = 0
  let mut styles = 0
  let worksheets : Map[String, Bool] = Map([])
  for relation in pkg.audit.relationships {
    if relation.kind == office_rel + "worksheet" {
      guard !worksheets.contains(relation.resolved) else {
        raise Invalid("multiple sheets cannot reuse one worksheet target")
      }
      worksheets[relation.resolved] = true
    } else if relation.kind == office_rel + "sharedStrings" {
      strings = pkg.xml_nodes[relation.resolved]
        .filter(fn(n) { n.depth == 1 && n.element.name.local_name == "si" })
        .length()
    } else if relation.kind == office_rel + "styles" {
      let mut inside = false
      for node in pkg.xml_nodes[relation.resolved] {
        if node.depth == 1 {
          inside = node.element.name.local_name == "cellXfs"
        } else if inside &&
          node.depth == 2 &&
          node.element.name.local_name == "xf" {
          styles += 1
        }
      }
    }
  }
  for relation in pkg.audit.relationships {
    if relation.kind != office_rel + "worksheet" {
      continue
    }
    let mut cell_type = "n"
    for node in pkg.xml_nodes[relation.resolved] {
      let name = node.element.name.local_name
      if name == "c" {
        cell_type = attr_opt(node, "t").unwrap_or("n")
        guard ["n", "s", "inlineStr", "b"].contains(cell_type) else {
          raise Refused("unsupported cell value type")
        }
        if attr_opt(node, "s") is Some(index) {
          ignore(checked_index(index, styles))
        }
      } else if name == "v" {
        let value = node.text_fragments.join("")
        if cell_type == "s" {
          ignore(checked_index(value, strings))
        } else if cell_type == "b" {
          guard value == "0" || value == "1" else {
            raise Invalid("invalid boolean cell value")
          }
        } else if cell_type == "inlineStr" {
          raise Invalid("inline string has a numeric value child")
        }
      } else if name == "is" && cell_type != "inlineStr" {
        raise Invalid("inline string/cell type mismatch")
      }
    }
  }
}