///|
/// An Ion value that has no PartiQL equivalent.
pub(all) suberror LowerError {
  /// A symbol whose text is unknown (a symbol ID that no table defines).
  SymbolWithoutText
} derive(Debug, Eq)

///|
/// Converts an Ion timestamp: missing fields become 0, an unknown offset
/// becomes UTC, and more than 9 fractional digits are truncated.
pub fn timestamp_of_ion(ts : @ion_core.Timestamp) -> Timestamp {
  let (nanos, precision) = match ts.fraction {
    None => (0, 0)
    Some(fraction) => {
      // Int64: `-exponent` must not overflow.
      let digits = -fraction.exponent.to_int64()
      if digits <= 0L {
        (0, 0)
      } else if digits <= 9L {
        let d = digits.to_int()
        let scale = @bigint.BigInt::from_int(pow10(9 - d))
        ((fraction.coefficient * scale).to_int(), d)
      } else {
        // Dropping more digits than the coefficient has leaves 0; never
        // build a power of ten larger than the coefficient.
        let drop = digits - 9L
        if drop > digit_count(fraction.coefficient).to_int64() {
          (0, 9)
        } else {
          let scale = 10N.pow(@bigint.BigInt::from_int64(drop))
          ((fraction.coefficient / scale).to_int(), 9)
        }
      }
    }
  }
  let offset = match ts.offset {
    Some(Known(minutes)) => minutes
    _ => 0
  }
  // An Ion timestamp is always valid, so these constructors do not raise.
  let date = Date::{
    year: ts.year,
    month: ts.month.unwrap_or(1),
    day: ts.day.unwrap_or(1),
  }
  let time = Time::{
    hour: ts.hour.unwrap_or(0),
    minute: ts.minute.unwrap_or(0),
    second: ts.second.unwrap_or(0),
    nanos,
    precision,
    offset: Some(offset),
  }
  { date, time, }
}

///|
fn lower_ion(x : @ion_core.IonValue) -> Value raise LowerError {
  match x.kind() {
    Null(_) => Null
    Bool(b) => Value::from_bool(b)
    Int(n) => Value::from_bigint(n)
    Float(d) => Value::from_float(d)
    Decimal(d) => Value::from_decimal(d)
    Timestamp(ts) => Value::from_timestamp(timestamp_of_ion(ts))
    Symbol(token) =>
      match token.text() {
        Some(text) => Value::from_string(text)
        None => raise SymbolWithoutText
      }
    String(s) => Value::from_string(s)
    Clob(bytes) => Value::from_clob(bytes)
    Blob(bytes) => Value::from_blob(bytes)
    List(items) | Sexp(items) =>
      Value::from_list(items.map(item => Value::from_ion(item)))
    Struct(fields) => {
      let pairs : Array[(String, Value)] = []
      for field in fields {
        match field.name().text() {
          Some(name) => pairs.push((name, Value::from_ion(field.value())))
          None => raise SymbolWithoutText
        }
      }
      Value::from_tuple(Tuple(pairs))
    }
  }
}

///|
/// Converts a boxed Ion value to its core value, one level deep. Other
/// values are returned unchanged.
pub fn Value::lower(self : Value) -> Value raise LowerError {
  match self {
    Ion(x) => lower_ion(x)
    other => other
  }
}

///|
/// Lowers recursively, including the children of collections and tuples.
pub fn Value::lower_deep(self : Value) -> Value raise LowerError {
  self.fold(
    (node, children) => {
      let node = node.lower()
      match node {
        List(_) => Value::from_list(children)
        Bag(_) => Value::from_bag(Bag(children))
        Tuple(t) =>
          Value::from_tuple(
            Tuple(t.fields().mapi((i, p) => (p.0, children[i]))),
          )
        Map(m) =>
          Value::from_map({
            key_type: m.key_type,
            value_type: m.value_type,
            entries: m
            .entries()
            .mapi((i, _) => (children[2 * i], children[2 * i + 1])),
          })
        Graph(g) => {
          let mut at = 0
          let payload = (p : Value?) => {
            match p {
              None => None
              Some(_) => {
                let v = children[at]
                at += 1
                Some(v)
              }
            }
          }
          let nodes = g.nodes().map(n => { ..n, payload: payload(n.payload), })
          let edges = g.edges().map(e => { ..e, payload: payload(e.payload), })
          Value::from_graph({ nodes, edges, })
        }
        other => other
      }
    },
    prepare=lowered_or_self,
  )
}